diff --git a/.eslintrc b/.eslintrc.json similarity index 100% rename from .eslintrc rename to .eslintrc.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1a324740cbd..34edacfa967 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,10 @@ --- name: Bug report about: Create a report to help us improve - --- - + + - VSCode Version: diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 2043e48c7aa..a0435cb1d3e 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,9 +1,9 @@ --- name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/vscode. +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/visual-studio-code. --- 🚨 The issue tracker is not for questions 🚨 -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/vscode. +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/visual-studio-code. diff --git a/.github/calendar.yml b/.github/calendar.yml index a08deeaee6c..d7827edb02b 100644 --- a/.github/calendar.yml +++ b/.github/calendar.yml @@ -21,5 +21,11 @@ '2018-05-15 12:00, US/Pacific': 'development', '2018-05-28 18:00, US/Pacific': 'endgame', # 'release' not needed anymore, return to 'development' after releasing. - '2018-06-06 12:00, US/Pacific': 'development', + '2018-06-06 12:00, US/Pacific': 'development', # 1.24.0 released + '2018-06-25 18:00, US/Pacific': 'endgame', + '2018-07-05 12:00, US/Pacific': 'development', # 1.25.0 released + '2018-07-30 18:00, US/Pacific': 'endgame', + '2018-08-13 12:00, US/Pacific': 'development', # 1.26.0 released + '2018-08-27 18:00, US/Pacific': 'endgame', +# '2018-09-05 12:00, US/Pacific': 'development', # 1.27.0 released } diff --git a/.github/classifier.yml b/.github/classifier.yml index af9e2f84e41..994c12071a2 100644 --- a/.github/classifier.yml +++ b/.github/classifier.yml @@ -1,43 +1,69 @@ { perform: true, alwaysRequireAssignee: false, - labelsRequiringAssignee: [ feature-request ], + labelsRequiringAssignee: [], autoAssignees: { + L10N: [], + VIM: [], api: { assignees: [ jrieken ], assignLabel: false }, - color-picker: [], - css-less-sass: [ aeschli ], + cli: [], + color-palette: [], + config: [], + css-less-scss: [ aeschli ], + debug-console: [], debug: { assignees: [ weinand ], assignLabel: false }, diff-editor: [], + dropdown: [], editor: [], editor-1000-limit: [], editor-autoclosing: [], editor-autoindent: [], editor-brackets: [], editor-clipboard: [], + editor-code-actions: [], + editor-code-lens: [], + editor-color-picker: [], + editor-colors: [], editor-columnselect: [], + editor-commands: [], editor-contrib: [], - editor-core: [], - editor-find-widget: [], + editor-drag-and-drop: [], + editor-find: [], editor-folding: [], + editor-hover: [], editor-ime: [], editor-input: [], + editor-ligatures: [], + editor-links: [], editor-minimap: [], editor-multicursor: [], + editor-parameter-hints: [], + editor-rendering: [], editor-smooth: [], + editor-symbols: [], + editor-textbuffer: [], editor-wrapping: [], emmet: [ ramya-rao-a ], error-list: [], + explorer-custom: [], + extension-host: [], extensions: [], + file-decorations: [], file-encoding: { assignees: [], assignLabel: false }, + file-explorer: { + assignees: [ bpasero ], + assignLabel: false + }, + file-glob: [], file-io: { assignees: [], assignLabel: false @@ -46,45 +72,73 @@ assignees: [], assignLabel: false }, - file-explorer: { - assignees: [ isidorn ], - assignLabel: false - }, - format: [], + formatting: [], git: [ joaomoreno ], + grammar: [], hot-exit: [ Tyriar ], html: [ aeschli ], - i18n: [], install-update: [], integrated-terminal: [ Tyriar ], + integration-test: [], + intellisense-config: [], + issue-reporter: [ RMacfarlane ], javascript: [ mjbvz ], json: [], - keybindings: [], keyboard-layout: [], + keybindings: [], + keybindings-editor: [], + lang-diagnostics: [], languages basic: [], + list: [], + log: [], markdown: [ mjbvz ], + marketplace: [], + menus: [], merge-conflict: [ chrmarti ], multi-root: { assignees: [], assignLabel: false }, + os-integration: [], + outline: [], + output: [], perf-profile: [], + perf-bloat: [], + perf-startup: [], php: [ roblourens ], proxy: [], + quick-pick: [ chrmarti ], + release-notes: [], remote: { assignees: [ jrieken ], assignLabel: false }, + rename: [], + run-as-admin: [], + samples: [], scm: [], search: [ roblourens ], + search-replace: [], + settings-editor: [], + shared-process: [], + smart-select: [], + smoke-test: [], snippets: { assignees: [ jrieken ], assignLabel: false }, + suggest: [], tasks: [ dbaeumer ], telemetry: [], themes: [], + tokenization: [], + tree: [], typescript: [ mjbvz ], + unit-test: [], + uri: [], + ux: [], + vscode-build: [], + webview: [], workbench: { assignees: [], assignLabel: false @@ -109,6 +163,10 @@ assignees: [], assignLabel: false }, + workbench-grid: { + assignees: [], + assignLabel: false + }, workbench-history: { assignees: [], assignLabel: false @@ -145,6 +203,10 @@ assignees: [], assignLabel: false }, + workbench-views: { + assignees: [], + assignLabel: false + }, workbench-welcome: [ chrmarti ] } } diff --git a/.github/commands.yml b/.github/commands.yml index ba8c8d7f3ce..56edba1d4da 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -29,7 +29,7 @@ type: 'label', name: '*out-of-scope', action: 'close', - comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that have been on the backlog for a long time but have not gained traction: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" + comment: "This issue is being closed to keep the number of issues in our inbox on a manageable level, we are closing issues that are not going to be addressed in the foreseeable future: We look at the number of votes the issue has received and the number of duplicate issues filed. If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" }, { type: 'label', @@ -62,5 +62,12 @@ action: 'comment', comment: "Potential duplicates:\n${potentialDuplicates}" }, + { + type: 'comment', + name: 'needsMoreInfo', + action: 'updateLabels', + addLabel: 'needs more info', + comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!" + }, ] } diff --git a/.github/new_release.yml b/.github/new_release.yml index a17089e852d..7482b60b108 100644 --- a/.github/new_release.yml +++ b/.github/new_release.yml @@ -1,6 +1,6 @@ { newReleaseLabel: 'new release', newReleaseColor: '006b75', - daysAfterRelease: 7, + daysAfterRelease: 5, perform: true -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index ccf4cca05f5..6a9804cd237 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,12 @@ npm-debug.log Thumbs.db node_modules/ .build/ +extensions/**/dist/ out/ out-build/ out-editor/ +out-editor-src/ +out-editor-build/ out-editor-esm/ out-editor-min/ out-monaco-editor-core/ @@ -14,4 +17,5 @@ out-vscode-min/ build/node_modules coverage/ test_data/ -yarn-error.log \ No newline at end of file +test-results/ +yarn-error.log diff --git a/.vscode/launch.json b/.vscode/launch.json index 4761367d036..73b99e84fbb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,6 +19,7 @@ "protocol": "inspector", "port": 5870, "restart": true, + "smartStep": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] @@ -29,6 +30,7 @@ "name": "Attach to Shared Process", "protocol": "inspector", "port": 5871, + "smartStep": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] @@ -39,6 +41,7 @@ "protocol": "inspector", "name": "Attach to Search Process", "port": 5876, + "smartStep": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] @@ -49,6 +52,7 @@ "name": "Attach to CLI Process", "protocol": "inspector", "port": 5874, + "smartStep": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] @@ -59,6 +63,7 @@ "name": "Attach to Main Process", "protocol": "inspector", "port": 5875, + "smartStep": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] @@ -124,6 +129,7 @@ "type": "chrome", "request": "attach", "name": "Attach to VS Code", + "smartStep": true, "port": 9222 }, { @@ -139,10 +145,11 @@ "linux": { "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" }, - "urlFilter": "*index.html*", + "urlFilter": "*workbench.html*", "runtimeArgs": [ "--inspect=5875" ], + "smartStep": true, "skipFiles": [ "**/winjs*.js" ], @@ -201,7 +208,7 @@ "linux": { "runtimeExecutable": "${workspaceFolder}/.build/electron/code-oss" }, - "stopOnEntry": false, + "outputCapture": "std", "args": [ "--delay", "--timeout", @@ -235,6 +242,15 @@ "VSCODE_DEV": "1", "VSCODE_CLI": "1" } + }, + { + "name": "Launch Built-in Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}/extensions/debug-auto-launch" + ] } ], "compounds": [ diff --git a/.yarnrc b/.yarnrc index 42f08fa0c02..58d37cc88fb 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "1.7.12" +target "2.0.7" runtime "electron" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09ee1ca9270..f3166331378 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ Please include the following with each issue: * What you expected to see, versus what you actually saw -* Images, animations, or a link to a video showing the issue occuring +* Images, animations, or a link to a video showing the issue occurring * A code snippet that demonstrates the issue or a link to a code repository the developers can easily pull down to recreate the issue locally @@ -81,7 +81,7 @@ Don't feel bad if the developers can't reproduce the issue right away. They will ### Follow Your Issue -Once submitted, your report will go into the [issue tracking](https://github.com/Microsoft/vscode/wiki/Issue-Tracking) work flow. Be sure to understand what will happen next, so you know what to expect, and how to continue to assist throughout the process. +Once submitted, your report will go into the [issue tracking](https://github.com/Microsoft/vscode/wiki/Issue-Tracking) workflow. Be sure to understand what will happen next, so you know what to expect, and how to continue to assist throughout the process. ## Automated Issue Management diff --git a/OSSREADME.json b/OSSREADME.json index 8f167afc904..e15cccabcad 100644 --- a/OSSREADME.json +++ b/OSSREADME.json @@ -2,7 +2,7 @@ [ { "name": "chromium", - "version": "58.0.3029.110", + "version": "61.0.3163.100", "repositoryURL": "http://www.chromium.org/Home", "licenseDetail": [ "BSD License", @@ -38,20 +38,20 @@ }, { "name": "libchromiumcontent", - "version": "58.0.3029.110", + "version": "61.0.3163.100", "license": "MIT", "repositoryURL": "https://github.com/electron/libchromiumcontent", "isProd": true }, { "name": "nodejs", - "version": "7.9.0", + "version": "8.9.3", "repositoryURL": "https://github.com/nodejs/node", "isProd": true }, { "name": "electron", - "version": "1.7.3", + "version": "2.0.7", "license": "MIT", "repositoryURL": "https://github.com/electron/electron", "isProd": true diff --git a/README.md b/README.md index c44f8166301..7b861aad8c9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Visual Studio Code - Open Source -[![Build Status](https://vscode.visualstudio.com/_apis/public/build/definitions/a4cdce18-a05c-4bb8-9476-5d07e63bfd76/1/badge)](https://aka.ms/vscode-builds) -[![Coverage Status](https://img.shields.io/coveralls/Microsoft/vscode/master.svg)](https://coveralls.io/github/Microsoft/vscode?branch=master) +[![Build Status](https://vscode.visualstudio.com/_apis/public/build/definitions/a4cdce18-a05c-4bb8-9476-5d07e63bfd76/1/badge?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?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Afeature-request) +[![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-blue.svg)](https://gitter.im/Microsoft/vscode) [VS Code](https://code.visualstudio.com) is a new type of tool that combines the simplicity of @@ -16,9 +17,9 @@ VS Code is updated monthly with new features and bug fixes. You can download it The [`vscode`](https://github.com/microsoft/vscode) repository is where we do development and there are many ways you can participate in the project, for example: -* [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues) and help us verify as they are checked in -* Review [source code changes](https://github.com/microsoft/vscode/pulls) -* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to new content +* [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues) and help us verify as they are checked in. +* Review [source code changes](https://github.com/microsoft/vscode/pulls). +* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to new content. ## Contributing @@ -42,6 +43,7 @@ Please see also our [Code of Conduct](CODE_OF_CONDUCT.md). * [Tweet](https://twitter.com/code) us with other feedback. ## Related Projects + Many of the core components and extensions to Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug). For a complete list, please see the [Related Projects](https://github.com/Microsoft/vscode/wiki/Related-Projects) page on our wiki. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index b2fe9e4c585..80e9fd6e808 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -21,48 +21,51 @@ This project incorporates components from the projects listed below. The origina 14. davidrios/pug-tmbundle (https://github.com/davidrios/pug-tmbundle) 15. definitelytyped (https://github.com/DefinitelyTyped/DefinitelyTyped) 16. demyte/language-cshtml (https://github.com/demyte/language-cshtml) -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) -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) -33. mdn-data version 1.1.12 (https://github.com/mdn/data) -34. Microsoft/TypeScript-TmLanguage version 0.0.1 (https://github.com/Microsoft/TypeScript-TmLanguage) -35. Microsoft/vscode-JSON.tmLanguage (https://github.com/Microsoft/vscode-JSON.tmLanguage) -36. Microsoft/vscode-mssql (https://github.com/Microsoft/vscode-mssql) -37. mmims/language-batchfile (https://github.com/mmims/language-batchfile) -38. octicons-code version 3.1.0 (https://octicons.github.com) -39. octicons-font version 3.1.0 (https://octicons.github.com) -40. PowerShell/EditorSyntax (https://github.com/powershell/editorsyntax) -41. seti-ui version 0.1.0 (https://github.com/jesseweed/seti-ui) -42. shaders-tmLanguage version 0.1.0 (https://github.com/tgjones/shaders-tmLanguage) -43. textmate/asp.vb.net.tmbundle (https://github.com/textmate/asp.vb.net.tmbundle) -44. textmate/c.tmbundle (https://github.com/textmate/c.tmbundle) -45. textmate/diff.tmbundle (https://github.com/textmate/diff.tmbundle) -46. textmate/git.tmbundle (https://github.com/textmate/git.tmbundle) -47. textmate/groovy.tmbundle (https://github.com/textmate/groovy.tmbundle) -48. textmate/html.tmbundle (https://github.com/textmate/html.tmbundle) -49. textmate/ini.tmbundle (https://github.com/textmate/ini.tmbundle) -50. textmate/javascript.tmbundle (https://github.com/textmate/javascript.tmbundle) -51. textmate/lua.tmbundle (https://github.com/textmate/lua.tmbundle) -52. textmate/markdown.tmbundle (https://github.com/textmate/markdown.tmbundle) -53. textmate/perl.tmbundle (https://github.com/textmate/perl.tmbundle) -54. textmate/ruby.tmbundle (https://github.com/textmate/ruby.tmbundle) -55. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) -56. TypeScript-TmLanguage version 0.1.8 (https://github.com/Microsoft/TypeScript-TmLanguage) -57. vscode-logfile-highlighter version 1.2.0 (https://github.com/emilast/vscode-logfile-highlighter) -58. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) +17. Document Object Model () +18. dotnet/csharp-tmLanguage version 0.1.0 (https://github.com/dotnet/csharp-tmLanguage) +19. expand-abbreviation version 0.5.8 (https://github.com/emmetio/expand-abbreviation) +20. fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) +21. freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) +22. HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) +23. Ikuyadeu/vscode-R (https://github.com/Ikuyadeu/vscode-R) +24. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) +25. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) +26. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) +27. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) +28. language-docker (https://github.com/moby/moby) +29. language-go version 0.39.0 (https://github.com/atom/language-go) +30. language-less (https://github.com/atom/language-less) +31. language-php (https://github.com/atom/language-php) +32. language-rust version 0.4.9 (https://github.com/zargony/atom-language-rust) +33. MagicStack/MagicPython (https://github.com/MagicStack/MagicPython) +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://octicons.github.com) +40. octicons-font version 3.1.0 (https://octicons.github.com) +41. PowerShell/EditorSyntax (https://github.com/powershell/editorsyntax) +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) +45. textmate/c.tmbundle (https://github.com/textmate/c.tmbundle) +46. textmate/diff.tmbundle (https://github.com/textmate/diff.tmbundle) +47. textmate/git.tmbundle (https://github.com/textmate/git.tmbundle) +48. textmate/groovy.tmbundle (https://github.com/textmate/groovy.tmbundle) +49. textmate/html.tmbundle (https://github.com/textmate/html.tmbundle) +50. textmate/ini.tmbundle (https://github.com/textmate/ini.tmbundle) +51. textmate/javascript.tmbundle (https://github.com/textmate/javascript.tmbundle) +52. textmate/lua.tmbundle (https://github.com/textmate/lua.tmbundle) +53. textmate/markdown.tmbundle (https://github.com/textmate/markdown.tmbundle) +54. textmate/perl.tmbundle (https://github.com/textmate/perl.tmbundle) +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 () +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) %% atom/language-c NOTICES AND INFORMATION BEGIN HERE @@ -600,6 +603,27 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF demyte/language-cshtml NOTICES AND INFORMATION +%% Document Object Model NOTICES AND INFORMATION BEGIN HERE +========================================= +W3C License +This work is being provided by the copyright holders under the following license. +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following +on ALL copies of the work or portions thereof, including modifications: +* The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +* Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +* Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived +from Document Object Model. Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS + AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR +FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. +Title to copyright in this work will at all times remain with copyright holders. +========================================= +END OF Document Object Model NOTICES AND INFORMATION + %% dotnet/csharp-tmLanguage NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -831,7 +855,7 @@ END OF ionide/ionide-fsgrammar NOTICES AND INFORMATION ========================================= The MIT License (MIT) -Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors. +Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors. 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: @@ -2225,6 +2249,66 @@ THE SOFTWARE. ========================================= END OF TypeScript-TmLanguage NOTICES AND INFORMATION +%% Unicode NOTICES AND INFORMATION BEGIN HERE +========================================= +Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +Unicode Data Files do not include PDF online code charts under the +directory http://www.unicode.org/Public/. + +Software includes any source code published in the Unicode Standard +or under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +NOTICE TO USER: Carefully read the following legal agreement. +BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S +DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), +YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. +IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE +THE DATA FILES OR SOFTWARE. + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1991-2017 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. +========================================= +END OF Unicode NOTICES AND INFORMATION + %% vscode-logfile-highlighter NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -2274,4 +2358,210 @@ 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-swift NOTICES AND INFORMATION \ No newline at end of file +END OF vscode-swift NOTICES AND INFORMATION + +%% Web Background Synchronization NOTICES AND INFORMATION BEGIN HERE +========================================= +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF Web Background Synchronization NOTICES AND INFORMATION \ No newline at end of file diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 795500b06eb..0fba2a82a6f 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,12 +1,12 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.24.0", + "version": "1.28.1", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { "name": "ms-vscode.node-debug2", - "version": "1.25.0", + "version": "1.28.0", "repo": "https://github.com/Microsoft/vscode-node-debug2" } ] diff --git a/build/dependencies.js b/build/dependencies.js index a0b1ee01dc5..2adf7e29ca7 100644 --- a/build/dependencies.js +++ b/build/dependencies.js @@ -43,7 +43,7 @@ function asYarnDependency(prefix, tree) { } function getYarnProductionDependencies(cwd) { - const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, 'ignore'] }); + const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, 'inherit'] }); const match = /^{"type":"tree".*$/m.exec(raw); if (!match || match.length !== 1) { diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 3fc14da5045..0c7a37b1f76 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -12,10 +12,12 @@ const File = require('vinyl'); const i18n = require('./lib/i18n'); const standalone = require('./lib/standalone'); const cp = require('child_process'); +const compilation = require('./lib/compilation'); +const monacoapi = require('./monaco/api'); +const fs = require('fs'); var root = path.dirname(__dirname); var sha1 = util.getVersion(root); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file var semver = require('./monaco/package.json').version; var headerVersion = semver + '(' + sha1 + ')'; @@ -59,29 +61,56 @@ var BUNDLED_FILE_HEADER = [ '' ].join('\n'); -function editorLoaderConfig() { - var result = common.loaderConfig(); - - // never ship octicons in editor - result.paths['vs/base/browser/ui/octiconLabel/octiconLabel'] = 'out-build/vs/base/browser/ui/octiconLabel/octiconLabel.mock'; - - // force css inlining to use base64 -- see https://github.com/Microsoft/monaco-editor/issues/148 - result['vs/css'] = { - inlineResources: 'base64', - inlineResourcesLimit: 3000 // see https://github.com/Microsoft/monaco-editor/issues/336 - }; - - return result; -} - const languages = i18n.defaultLanguages.concat([]); // i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); +gulp.task('clean-editor-src', util.rimraf('out-editor-src')); +gulp.task('extract-editor-src', ['clean-editor-src'], function () { + console.log(`If the build fails, consider tweaking shakeLevel below to a lower value.`); + const apiusages = monacoapi.execute().usageContent; + const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); + standalone.extractEditor({ + sourcesRoot: path.join(root, 'src'), + entryPoints: [ + 'vs/editor/editor.main', + 'vs/editor/editor.worker', + 'vs/base/worker/workerMain', + ], + inlineEntryPoints: [ + apiusages, + extrausages + ], + libs: [ + `lib.d.ts`, + `lib.es2015.collection.d.ts` + ], + redirects: { + 'vs/base/browser/ui/octiconLabel/octiconLabel': 'vs/base/browser/ui/octiconLabel/octiconLabel.mock', + }, + compilerOptions: { + module: 2, // ModuleKind.AMD + }, + shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers + importIgnorePattern: /^vs\/css!/, + destRoot: path.join(root, 'out-editor-src') + }); +}); + +// Full compile, including nls and inline sources in sourcemaps, for build +gulp.task('clean-editor-build', util.rimraf('out-editor-build')); +gulp.task('compile-editor-build', ['clean-editor-build', 'extract-editor-src'], compilation.compileTask('out-editor-src', 'out-editor-build', true)); + gulp.task('clean-optimized-editor', util.rimraf('out-editor')); -gulp.task('optimize-editor', ['clean-optimized-editor', 'compile-client-build'], common.optimizeTask({ +gulp.task('optimize-editor', ['clean-optimized-editor', 'compile-editor-build'], common.optimizeTask({ + src: 'out-editor-build', entryPoints: editorEntryPoints, otherSources: editorOtherSources, resources: editorResources, - loaderConfig: editorLoaderConfig(), + loaderConfig: { + paths: { + 'vs': 'out-editor-build/vs', + 'vscode': 'empty:' + } + }, bundleLoader: false, header: BUNDLED_FILE_HEADER, bundleInfo: true, @@ -93,17 +122,25 @@ gulp.task('clean-minified-editor', util.rimraf('out-editor-min')); gulp.task('minify-editor', ['clean-minified-editor', 'optimize-editor'], common.minifyTask('out-editor')); gulp.task('clean-editor-esm', util.rimraf('out-editor-esm')); -gulp.task('extract-editor-esm', ['clean-editor-esm', 'clean-editor-distro'], function () { - standalone.createESMSourcesAndResources({ - entryPoints: [ - 'vs/editor/editor.main', - 'vs/editor/editor.worker' - ], +gulp.task('extract-editor-esm', ['clean-editor-esm', 'clean-editor-distro', 'extract-editor-src'], function () { + standalone.createESMSourcesAndResources2({ + srcFolder: './out-editor-src', outFolder: './out-editor-esm/src', outResourcesFolder: './out-monaco-editor-core/esm', - redirects: { - 'vs/base/browser/ui/octiconLabel/octiconLabel': 'vs/base/browser/ui/octiconLabel/octiconLabel.mock', - 'vs/nls': 'vs/nls.mock', + ignores: [ + 'inlineEntryPoint:0.ts', + 'inlineEntryPoint:1.ts', + 'vs/loader.js', + 'vs/nls.ts', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/css.js', + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/base/worker/workerMain.ts', + ], + renames: { + 'vs/nls.mock.ts': 'vs/nls.ts' } }); }); @@ -165,7 +202,7 @@ gulp.task('editor-distro', ['clean-editor-distro', 'compile-editor-esm', 'minify this.emit('data', new File({ path: data.path.replace(/monaco\.d\.ts/, 'editor.api.d.ts'), base: data.base, - contents: new Buffer(toExternalDTS(data.contents.toString())) + contents: Buffer.from(toExternalDTS(data.contents.toString())) })); })) .pipe(gulp.dest('out-monaco-editor-core/esm/vs/editor')), @@ -230,7 +267,7 @@ gulp.task('editor-distro', ['clean-editor-distro', 'compile-editor-esm', 'minify }); gulp.task('analyze-editor-distro', function () { - // @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file + // @ts-ignore var bundleInfo = require('../out-editor/bundleInfo.json'); var graph = bundleInfo.graph; var bundles = bundleInfo.bundles; diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 3ec638c2f22..bd38427a422 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -20,7 +20,6 @@ const sourcemaps = require('gulp-sourcemaps'); const nlsDev = require('vscode-nls-dev'); const root = path.dirname(__dirname); const commit = util.getVersion(root); -const i18n = require('./lib/i18n'); const plumber = require('gulp-plumber'); const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); @@ -32,8 +31,6 @@ const compilations = glob.sync('**/tsconfig.json', { const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; -const languages = i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); - const tasks = compilations.map(function (tsconfigFile) { const absolutePath = path.join(extensionsPath, tsconfigFile); const relativeDirname = path.dirname(tsconfigFile); @@ -58,7 +55,6 @@ const tasks = compilations.map(function (tsconfigFile) { const srcBase = path.join(root, 'src'); const src = path.join(srcBase, '**'); const out = path.join(root, 'out'); - const i18nPath = path.join(__dirname, '..', 'i18n'); const baseUrl = getBaseUrl(out); let headerId, headerOut; @@ -102,9 +98,9 @@ const tasks = compilations.map(function (tsconfigFile) { sourceRoot: '../src' })) .pipe(tsFilter.restore) - .pipe(build ? nlsDev.createAdditionalLanguageFiles(languages, i18nPath, out) : es.through()) .pipe(build ? nlsDev.bundleMetaDataFiles(headerId, headerOut) : es.through()) - .pipe(build ? nlsDev.bundleLanguageFiles() : es.through()) + // Filter out *.nls.json file. We needed them only to bundle meta data file. + .pipe(filter(['**', '!**/*.nls.json'])) .pipe(reporter.end(emitError)); return es.duplex(input, output); @@ -171,4 +167,4 @@ gulp.task('watch-extensions', tasks.map(t => t.watch)); gulp.task('clean-extensions-build', tasks.map(t => t.cleanBuild)); gulp.task('compile-extensions-build', tasks.map(t => t.compileBuild)); -gulp.task('watch-extensions-build', tasks.map(t => t.watchBuild)); \ No newline at end of file +gulp.task('watch-extensions-build', tasks.map(t => t.watchBuild)); diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index a84e78dece1..893568ca9c5 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -44,7 +44,9 @@ const indentationFilter = [ '!ThirdPartyNotices.txt', '!LICENSE.txt', '!src/vs/nls.js', + '!src/vs/nls.build.js', '!src/vs/css.js', + '!src/vs/css.build.js', '!src/vs/loader.js', '!src/vs/base/common/marked/marked.js', '!src/vs/base/common/winjs.base.js', @@ -82,6 +84,7 @@ const indentationFilter = [ '!build/{lib,tslintRules}/**/*.js', '!build/**/*.sh', '!build/tfs/**/*.js', + '!build/tfs/**/*.config', '!**/Dockerfile', '!extensions/markdown-language-features/media/*.js' ]; @@ -104,6 +107,7 @@ const copyrightFilter = [ '!**/*.code-workspace', '!build/**/*.init', '!resources/linux/snap/snapcraft.yaml', + '!resources/linux/snap/electron-launch', '!resources/win32/bin/code.js', '!extensions/markdown-language-features/media/highlight.css', '!extensions/html-language-features/server/src/modes/typescript/*', diff --git a/build/gulpfile.mixin.js b/build/gulpfile.mixin.js index a370981ab3c..29cce77c5dd 100644 --- a/build/gulpfile.mixin.js +++ b/build/gulpfile.mixin.js @@ -15,7 +15,6 @@ const remote = require('gulp-remote-src'); const zip = require('gulp-vinyl-zip'); const assign = require('object-assign'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const pkg = require('../package.json'); gulp.task('mixin', function () { @@ -56,7 +55,6 @@ gulp.task('mixin', function () { .pipe(util.rebase(2)) .pipe(productJsonFilter) .pipe(buffer()) - // @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file .pipe(json(o => assign({}, require('../product.json'), o))) .pipe(productJsonFilter.restore); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 30a1563d7b9..46c045e0e89 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -17,23 +17,18 @@ const vfs = require('vinyl-fs'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); const filter = require('gulp-filter'); -const buffer = require('gulp-buffer'); const json = require('gulp-json-editor'); const _ = require('underscore'); const util = require('./lib/util'); const ext = require('./lib/extensions'); const buildfile = require('../src/buildfile'); const common = require('./lib/optimize'); -const nlsDev = require('vscode-nls-dev'); const root = path.dirname(__dirname); const commit = util.getVersion(root); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const packageJson = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); const crypto = require('crypto'); const i18n = require('./lib/i18n'); -const glob = require('glob'); const deps = require('./dependencies'); const getElectronVersion = require('./lib/electron').getElectronVersion; const createAsar = require('./lib/asar').createAsar; @@ -42,21 +37,12 @@ const productionDependencies = deps.getProductionDependencies(path.dirname(__dir // @ts-ignore const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); const nodeModules = ['electron', 'original-fs'] + // @ts-ignore JSON checking: dependencies property is optional .concat(Object.keys(product.dependencies || {})) .concat(_.uniq(productionDependencies.map(d => d.name))) .concat(baseModules); // Build -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file -const builtInExtensions = require('./builtInExtensions.json'); - -const excludedExtensions = [ - 'vscode-api-tests', - 'vscode-colorize-tests', - 'ms-vscode.node-debug', - 'ms-vscode.node-debug2', -]; - const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.main'), buildfile.base, @@ -69,23 +55,24 @@ const vscodeResources = [ 'out-build/cli.js', 'out-build/driver.js', 'out-build/bootstrap.js', + 'out-build/bootstrap-fork.js', 'out-build/bootstrap-amd.js', + 'out-build/bootstrap-window.js', 'out-build/paths.js', 'out-build/vs/**/*.{svg,png,cur,html}', 'out-build/vs/base/common/performance.js', - 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh, cpuUsage.sh}', + 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh}', 'out-build/vs/base/browser/ui/octiconLabel/octicons/**', 'out-build/vs/workbench/browser/media/*-theme.css', - 'out-build/vs/workbench/electron-browser/bootstrap/**', 'out-build/vs/workbench/parts/debug/**/*.json', 'out-build/vs/workbench/parts/execution/**/*.scpt', 'out-build/vs/workbench/parts/webview/electron-browser/webview-pre.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/parts/tasks/**/*.json', - 'out-build/vs/workbench/parts/terminal/electron-browser/terminalProcess.js', 'out-build/vs/workbench/parts/welcome/walkThrough/**/*.md', 'out-build/vs/workbench/services/files/**/*.exe', 'out-build/vs/workbench/services/files/**/*.md', + 'out-build/vs/code/electron-browser/workbench/**', 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-browser/issue/issueReporter.js', 'out-build/vs/code/electron-browser/processExplorer/processExplorer.js', @@ -98,33 +85,33 @@ const BUNDLED_FILE_HEADER = [ ' *--------------------------------------------------------*/' ].join('\n'); -const languages = i18n.defaultLanguages.concat([]); // i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); - gulp.task('clean-optimized-vscode', util.rimraf('out-vscode')); gulp.task('optimize-vscode', ['clean-optimized-vscode', 'compile-build', 'compile-extensions-build'], common.optimizeTask({ + src: 'out-build', entryPoints: vscodeEntryPoints, otherSources: [], resources: vscodeResources, loaderConfig: common.loaderConfig(nodeModules), header: BUNDLED_FILE_HEADER, out: 'out-vscode', - languages: languages, bundleInfo: undefined })); gulp.task('optimize-index-js', ['optimize-vscode'], () => { - const fullpath = path.join(process.cwd(), 'out-vscode/vs/workbench/electron-browser/bootstrap/index.js'); + const fullpath = path.join(process.cwd(), 'out-vscode/vs/code/electron-browser/workbench/workbench.js'); const contents = fs.readFileSync(fullpath).toString(); const newContents = contents.replace('[/*BUILD->INSERT_NODE_MODULES*/]', JSON.stringify(nodeModules)); fs.writeFileSync(fullpath, newContents); }); -const baseUrl = `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`; +const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; gulp.task('clean-minified-vscode', util.rimraf('out-vscode-min')); -gulp.task('minify-vscode', ['clean-minified-vscode', 'optimize-index-js'], common.minifyTask('out-vscode', baseUrl)); +gulp.task('minify-vscode', ['clean-minified-vscode', 'optimize-index-js'], common.minifyTask('out-vscode', `${sourceMappingURLBase}/core`)); // Package + +// @ts-ignore JSON checking: darwinCredits is optional const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); const config = { @@ -153,6 +140,8 @@ const config = { linuxExecutableName: product.applicationName, winIcon: 'resources/win32/code.ico', token: process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN'] || void 0, + + // @ts-ignore JSON checking: electronRepository is optional repo: product.electronRepository || void 0 }; @@ -225,48 +214,23 @@ function packageTask(platform, arch, opts) { const checksums = computeChecksums(out, [ 'vs/workbench/workbench.main.js', 'vs/workbench/workbench.main.css', - 'vs/workbench/electron-browser/bootstrap/index.html', - 'vs/workbench/electron-browser/bootstrap/index.js', - 'vs/workbench/electron-browser/bootstrap/preload.js' + 'vs/code/electron-browser/workbench/workbench.html', + 'vs/code/electron-browser/workbench/workbench.js' ]); const src = gulp.src(out + '/**', { base: '.' }) - .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + out), 'out'); })); - - const root = path.resolve(path.join(__dirname, '..')); - const localExtensionDescriptions = glob.sync('extensions/*/package.json') - .map(manifestPath => { - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); - return { name: extensionName, path: extensionPath }; - }) - .filter(({ name }) => excludedExtensions.indexOf(name) === -1) - .filter(({ name }) => builtInExtensions.every(b => b.name !== name)); - - const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { - const nlsFilter = filter('**/*.nls.json', { restore: true }); - - return ext.fromLocal(extension.path) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)) - // // TODO@Dirk: this filter / buffer is here to make sure the nls.json files are buffered - .pipe(nlsFilter) - .pipe(buffer()) - .pipe(nlsDev.createAdditionalLanguageFiles(languages, path.join(__dirname, '..', 'i18n'))) - .pipe(nlsFilter.restore); - })); - - const localExtensionDependencies = gulp.src('extensions/node_modules/**', { base: '.' }); - - const marketplaceExtensions = es.merge(...builtInExtensions.map(extension => { - return ext.fromMarketplace(extension.name, extension.version) - .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - })); - - const sources = es.merge(src, localExtensions, localExtensionDependencies, marketplaceExtensions) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + out), 'out'); })) .pipe(util.setExecutableBit(['**/*.sh'])) .pipe(filter(['**', '!**/*.js.map'])); + const root = path.resolve(path.join(__dirname, '..')); + + const sources = es.merge(src, ext.packageExtensionsStream({ + sourceMappingURLBase: sourceMappingURLBase + })); + let version = packageJson.version; + // @ts-ignore JSON checking: quality is optional const quality = product.quality; if (quality && quality !== 'stable') { @@ -277,10 +241,15 @@ function packageTask(platform, arch, opts) { const packageJsonStream = gulp.src(['package.json'], { base: '.' }) .pipe(json({ name, version })); - const settingsSearchBuildId = getSettingsSearchBuildId(packageJson); const date = new Date().toISOString(); + const productJsonUpdate = { commit, date, checksums }; + + if (shouldSetupSettingsSearch()) { + productJsonUpdate.settingsSearchBuildId = getSettingsSearchBuildId(packageJson); + } + const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date, checksums, settingsSearchBuildId })); + .pipe(json(productJsonUpdate)); const license = gulp.src(['LICENSES.chromium.html', 'LICENSE.txt', 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.' }); @@ -291,6 +260,7 @@ function packageTask(platform, arch, opts) { const depsSrc = [ ..._.flatten(productionDependencies.map(d => path.relative(root, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`])), + // @ts-ignore JSON checking: dependencies is optional ..._.flatten(Object.keys(product.dependencies || {}).map(d => [`node_modules/${d}/**`, `!node_modules/${d}/**/{test,tests}/**`])) ]; @@ -359,6 +329,15 @@ function packageTask(platform, arch, opts) { .pipe(rename('bin/' + product.applicationName))); } + // submit all stats that have been collected + // during the build phase + if (opts.stats) { + result.on('end', () => { + const { submitAllStats } = require('./lib/stats'); + submitAllStats(product, commit).then(() => console.log('Submitted bundle stats!')); + }); + } + return result.pipe(vfs.dest(destination)); }; } @@ -371,20 +350,23 @@ gulp.task('clean-vscode-darwin', util.rimraf(path.join(buildRoot, 'VSCode-darwin gulp.task('clean-vscode-linux-ia32', util.rimraf(path.join(buildRoot, 'VSCode-linux-ia32'))); gulp.task('clean-vscode-linux-x64', util.rimraf(path.join(buildRoot, 'VSCode-linux-x64'))); gulp.task('clean-vscode-linux-arm', util.rimraf(path.join(buildRoot, 'VSCode-linux-arm'))); +gulp.task('clean-vscode-linux-arm64', util.rimraf(path.join(buildRoot, 'VSCode-linux-arm64'))); gulp.task('vscode-win32-ia32', ['optimize-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32')); gulp.task('vscode-win32-x64', ['optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); -gulp.task('vscode-darwin', ['optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); +gulp.task('vscode-darwin', ['optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin', null, { stats: true })); gulp.task('vscode-linux-ia32', ['optimize-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32')); gulp.task('vscode-linux-x64', ['optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); gulp.task('vscode-linux-arm', ['optimize-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm')); +gulp.task('vscode-linux-arm64', ['optimize-vscode', 'clean-vscode-linux-arm64'], packageTask('linux', 'arm64')); gulp.task('vscode-win32-ia32-min', ['minify-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32', { minified: true })); gulp.task('vscode-win32-x64-min', ['minify-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64', { minified: true })); -gulp.task('vscode-darwin-min', ['minify-vscode', 'clean-vscode-darwin'], packageTask('darwin', null, { minified: true })); +gulp.task('vscode-darwin-min', ['minify-vscode', 'clean-vscode-darwin'], packageTask('darwin', null, { minified: true, stats: true })); gulp.task('vscode-linux-ia32-min', ['minify-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32', { minified: true })); gulp.task('vscode-linux-x64-min', ['minify-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64', { minified: true })); gulp.task('vscode-linux-arm-min', ['minify-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm', { minified: true })); +gulp.task('vscode-linux-arm64-min', ['minify-vscode', 'clean-vscode-linux-arm64'], packageTask('linux', 'arm64', { minified: true })); // Transifex Localizations @@ -434,37 +416,38 @@ gulp.task('vscode-translations-push-test', ['optimize-vscode'], function () { }); gulp.task('vscode-translations-pull', function () { - [...i18n.defaultLanguages, ...i18n.extraLanguages].forEach(language => { - i18n.pullCoreAndExtensionsXlfFiles(apiHostname, apiName, apiToken, language).pipe(vfs.dest(`../vscode-localization/${language.id}/build`)); - + return es.merge([...i18n.defaultLanguages, ...i18n.extraLanguages].map(language => { let includeDefault = !!innoSetupConfig[language.id].defaultInfo; - i18n.pullSetupXlfFiles(apiHostname, apiName, apiToken, language, includeDefault).pipe(vfs.dest(`../vscode-localization/${language.id}/setup`)); - }); + return i18n.pullSetupXlfFiles(apiHostname, apiName, apiToken, language, includeDefault).pipe(vfs.dest(`../vscode-localization/${language.id}/setup`)); + })); }); gulp.task('vscode-translations-import', function () { - [...i18n.defaultLanguages, ...i18n.extraLanguages].forEach(language => { - gulp.src(`../vscode-localization/${language.id}/build/*/*.xlf`) - .pipe(i18n.prepareI18nFiles()) - .pipe(vfs.dest(`./i18n/${language.folderName}`)); - gulp.src(`../vscode-localization/${language.id}/setup/*/*.xlf`) + return es.merge([...i18n.defaultLanguages, ...i18n.extraLanguages].map(language => { + return gulp.src(`../vscode-localization/${language.id}/setup/*/*.xlf`) .pipe(i18n.prepareIslFiles(language, innoSetupConfig[language.id])) .pipe(vfs.dest(`./build/win32/i18n`)); - }); + })); }); // Sourcemaps -gulp.task('upload-vscode-sourcemaps', ['minify-vscode'], () => { +gulp.task('upload-vscode-sourcemaps', ['vscode-darwin-min', 'minify-vscode'], () => { const vs = gulp.src('out-vscode-min/**/*.map', { base: 'out-vscode-min' }) .pipe(es.mapSync(f => { f.path = `${f.base}/core/${f.relative}`; return f; })); - const extensions = gulp.src('extensions/**/out/**/*.map', { base: '.' }); + const extensionsOut = gulp.src('extensions/**/out/**/*.map', { base: '.' }); + const extensionsDist = gulp.src('extensions/**/dist/**/*.map', { base: '.' }); - return es.merge(vs, extensions) + return es.merge(vs, extensionsOut, extensionsDist) + .pipe(es.through(function (data) { + // debug + console.log('Uploading Sourcemap', data.relative); + this.emit('data', data); + })) .pipe(azure.upload({ account: process.env.AZURE_STORAGE_ACCOUNT, key: process.env.AZURE_STORAGE_ACCESS_KEY, @@ -475,9 +458,8 @@ gulp.task('upload-vscode-sourcemaps', ['minify-vscode'], () => { const allConfigDetailsPath = path.join(os.tmpdir(), 'configuration.json'); gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () => { - const branch = process.env.BUILD_SOURCEBRANCH; - - if (!/\/master$/.test(branch) && branch.indexOf('/release/') < 0) { + if (!shouldSetupSettingsSearch()) { + const branch = process.env.BUILD_SOURCEBRANCH; console.log(`Only runs on master and release branches, not ${branch}`); return; } @@ -500,13 +482,24 @@ gulp.task('upload-vscode-configuration', ['generate-vscode-configuration'], () = })); }); -function getSettingsSearchBuildId(packageJson) { - const previous = util.getPreviousVersion(packageJson.version); +function shouldSetupSettingsSearch() { + const branch = process.env.BUILD_SOURCEBRANCH; + return branch && (/\/master$/.test(branch) || branch.indexOf('/release/') >= 0); +} +function getSettingsSearchBuildId(packageJson) { try { - const out = cp.execSync(`git rev-list ${previous}..HEAD --count`); + const branch = process.env.BUILD_SOURCEBRANCH; + const branchId = branch.indexOf('/release/') >= 0 ? 0 : + /\/master$/.test(branch) ? 1 : + 2; // Some unexpected branch + + const out = cp.execSync(`git rev-list HEAD --count`); const count = parseInt(out.toString()); - return util.versionStringToNumber(packageJson.version) * 1e4 + count; + + // + // 1.25.1, 1,234,567 commits, master = 1250112345671 + return util.versionStringToNumber(packageJson.version) * 1e8 + count * 10 + branchId; } catch (e) { throw new Error('Could not determine build number: ' + e.toString()); } @@ -520,6 +513,10 @@ gulp.task('generate-vscode-configuration', () => { return reject(new Error('$AGENT_BUILDDIRECTORY not set')); } + if (process.env.VSCODE_QUALITY !== 'insider' && process.env.VSCODE_QUALITY !== 'stable') { + return resolve(); + } + const userDataDir = path.join(os.tmpdir(), 'tmpuserdata'); const extensionsDir = path.join(os.tmpdir(), 'tmpextdir'); const appName = process.env.VSCODE_QUALITY === 'insider' ? 'Visual\\ Studio\\ Code\\ -\\ Insiders.app' : 'Visual\\ Studio\\ Code.app'; diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index ecbc45df320..ff4ba02d804 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -12,17 +12,14 @@ const shell = require('gulp-shell'); const es = require('event-stream'); const vfs = require('vinyl-fs'); const util = require('./lib/util'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const packageJson = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const rpmDependencies = require('../resources/linux/rpm/dependencies.json'); const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); function getDebPackageArch(arch) { - return { x64: 'amd64', ia32: 'i386', arm: 'armhf' }[arch]; + return { x64: 'amd64', ia32: 'i386', arm: 'armhf', arm64: "arm64" }[arch]; } function prepareDebPackage(arch) { @@ -75,7 +72,9 @@ function prepareDebPackage(arch) { const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@ARCHITECTURE@@', debArch)) + // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) + // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(rename('DEBIAN/postinst')); @@ -99,7 +98,7 @@ function getRpmBuildPath(rpmArch) { } function getRpmPackageArch(arch) { - return { x64: 'x86_64', ia32: 'i386', arm: 'armhf' }[arch]; + return { x64: 'x86_64', ia32: 'i386', arm: 'armhf', arm64: "arm64" }[arch]; } function prepareRpmPackage(arch) { @@ -133,7 +132,9 @@ function prepareRpmPackage(arch) { .pipe(replace('@@RELEASE@@', linuxPackageRevision)) .pipe(replace('@@ARCHITECTURE@@', rpmArch)) .pipe(replace('@@LICENSE@@', product.licenseName)) + // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) + // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', '))) .pipe(rename('SPECS/' + product.applicationName + '.spec')); @@ -207,33 +208,39 @@ function buildSnapPackage(arch) { gulp.task('clean-vscode-linux-ia32-deb', util.rimraf('.build/linux/deb/i386')); gulp.task('clean-vscode-linux-x64-deb', util.rimraf('.build/linux/deb/amd64')); gulp.task('clean-vscode-linux-arm-deb', util.rimraf('.build/linux/deb/armhf')); +gulp.task('clean-vscode-linux-arm64-deb', util.rimraf('.build/linux/deb/arm64')); gulp.task('clean-vscode-linux-ia32-rpm', util.rimraf('.build/linux/rpm/i386')); gulp.task('clean-vscode-linux-x64-rpm', util.rimraf('.build/linux/rpm/x86_64')); gulp.task('clean-vscode-linux-arm-rpm', util.rimraf('.build/linux/rpm/armhf')); +gulp.task('clean-vscode-linux-arm64-rpm', util.rimraf('.build/linux/rpm/arm64')); gulp.task('clean-vscode-linux-ia32-snap', util.rimraf('.build/linux/snap/x64')); gulp.task('clean-vscode-linux-x64-snap', util.rimraf('.build/linux/snap/x64')); gulp.task('clean-vscode-linux-arm-snap', util.rimraf('.build/linux/snap/x64')); -gulp.task('clean-vscode-linux-ia32-flatpak', util.rimraf('.build/linux/flatpak/i386')); -gulp.task('clean-vscode-linux-x64-flatpak', util.rimraf('.build/linux/flatpak/x86_64')); -gulp.task('clean-vscode-linux-arm-flatpak', util.rimraf('.build/linux/flatpak/arm')); +gulp.task('clean-vscode-linux-arm64-snap', util.rimraf('.build/linux/snap/x64')); gulp.task('vscode-linux-ia32-prepare-deb', ['clean-vscode-linux-ia32-deb'], prepareDebPackage('ia32')); gulp.task('vscode-linux-x64-prepare-deb', ['clean-vscode-linux-x64-deb'], prepareDebPackage('x64')); gulp.task('vscode-linux-arm-prepare-deb', ['clean-vscode-linux-arm-deb'], prepareDebPackage('arm')); +gulp.task('vscode-linux-arm64-prepare-deb', ['clean-vscode-linux-arm64-deb'], prepareDebPackage('arm64')); gulp.task('vscode-linux-ia32-build-deb', ['vscode-linux-ia32-prepare-deb'], buildDebPackage('ia32')); gulp.task('vscode-linux-x64-build-deb', ['vscode-linux-x64-prepare-deb'], buildDebPackage('x64')); gulp.task('vscode-linux-arm-build-deb', ['vscode-linux-arm-prepare-deb'], buildDebPackage('arm')); +gulp.task('vscode-linux-arm64-build-deb', ['vscode-linux-arm64-prepare-deb'], buildDebPackage('arm64')); gulp.task('vscode-linux-ia32-prepare-rpm', ['clean-vscode-linux-ia32-rpm'], prepareRpmPackage('ia32')); gulp.task('vscode-linux-x64-prepare-rpm', ['clean-vscode-linux-x64-rpm'], prepareRpmPackage('x64')); gulp.task('vscode-linux-arm-prepare-rpm', ['clean-vscode-linux-arm-rpm'], prepareRpmPackage('arm')); +gulp.task('vscode-linux-arm64-prepare-rpm', ['clean-vscode-linux-arm64-rpm'], prepareRpmPackage('arm64')); gulp.task('vscode-linux-ia32-build-rpm', ['vscode-linux-ia32-prepare-rpm'], buildRpmPackage('ia32')); gulp.task('vscode-linux-x64-build-rpm', ['vscode-linux-x64-prepare-rpm'], buildRpmPackage('x64')); gulp.task('vscode-linux-arm-build-rpm', ['vscode-linux-arm-prepare-rpm'], buildRpmPackage('arm')); +gulp.task('vscode-linux-arm64-build-rpm', ['vscode-linux-arm64-prepare-rpm'], buildRpmPackage('arm64')); gulp.task('vscode-linux-ia32-prepare-snap', ['clean-vscode-linux-ia32-snap'], prepareSnapPackage('ia32')); gulp.task('vscode-linux-x64-prepare-snap', ['clean-vscode-linux-x64-snap'], prepareSnapPackage('x64')); gulp.task('vscode-linux-arm-prepare-snap', ['clean-vscode-linux-arm-snap'], prepareSnapPackage('arm')); +gulp.task('vscode-linux-arm64-prepare-snap', ['clean-vscode-linux-arm64-snap'], prepareSnapPackage('arm64')); gulp.task('vscode-linux-ia32-build-snap', ['vscode-linux-ia32-prepare-snap'], buildSnapPackage('ia32')); gulp.task('vscode-linux-x64-build-snap', ['vscode-linux-x64-prepare-snap'], buildSnapPackage('x64')); gulp.task('vscode-linux-arm-build-snap', ['vscode-linux-arm-prepare-snap'], buildSnapPackage('arm')); +gulp.task('vscode-linux-arm64-build-snap', ['vscode-linux-arm64-prepare-snap'], buildSnapPackage('arm64')); diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 636d9e8300b..7d1ea35a6ab 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -7,44 +7,69 @@ const gulp = require('gulp'); const path = require('path'); +const fs = require('fs'); const assert = require('assert'); const cp = require('child_process'); const _7z = require('7zip')['7z']; const util = require('./lib/util'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const pkg = require('../package.json'); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const product = require('../product.json'); const vfs = require('vinyl-fs'); +const mkdirp = require('mkdirp'); const repoPath = path.dirname(__dirname); const buildPath = arch => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); const zipDir = arch => path.join(repoPath, '.build', `win32-${arch}`, 'archive'); const zipPath = arch => path.join(zipDir(arch), `VSCode-win32-${arch}.zip`); -const setupDir = arch => path.join(repoPath, '.build', `win32-${arch}`, 'setup'); +const setupDir = (arch, target) => path.join(repoPath, '.build', `win32-${arch}`, `${target}-setup`); const issPath = path.join(__dirname, 'win32', 'code.iss'); const innoSetupPath = path.join(path.dirname(path.dirname(require.resolve('innosetup-compiler'))), 'bin', 'ISCC.exe'); +const signPS1 = path.join(repoPath, 'build', 'tfs', 'win32', 'sign.ps1'); function packageInnoSetup(iss, options, cb) { options = options || {}; const definitions = options.definitions || {}; + const debug = process.argv.some(arg => arg === '--debug-inno'); + + if (debug) { + definitions['Debug'] = 'true'; + } + const keys = Object.keys(definitions); keys.forEach(key => assert(typeof definitions[key] === 'string', `Missing value for '${key}' in Inno Setup package step`)); const defs = keys.map(key => `/d${key}=${definitions[key]}`); - const args = [iss].concat(defs); + const args = [ + iss, + ...defs, + `/sesrp=powershell.exe -ExecutionPolicy bypass ${signPS1} $f` + ]; - cp.spawn(innoSetupPath, args, { stdio: 'inherit' }) + cp.spawn(innoSetupPath, args, { stdio: ['ignore', 'inherit', 'inherit'] }) .on('error', cb) .on('exit', () => cb(null)); } -function buildWin32Setup(arch) { +function buildWin32Setup(arch, target) { + if (target !== 'system' && target !== 'user') { + throw new Error('Invalid setup target'); + } + return cb => { - const ia32AppId = product.win32AppId; - const x64AppId = product.win32x64AppId; + const ia32AppId = target === 'system' ? product.win32AppId : product.win32UserAppId; + const x64AppId = target === 'system' ? product.win32x64AppId : product.win32x64UserAppId; + + const sourcePath = buildPath(arch); + const outputPath = setupDir(arch, target); + mkdirp.sync(outputPath); + + const originalProductJsonPath = path.join(sourcePath, 'resources/app/product.json'); + const productJsonPath = path.join(outputPath, 'product.json'); + const productJson = JSON.parse(fs.readFileSync(originalProductJsonPath, 'utf8')); + productJson['target'] = target; + fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); const definitions = { NameLong: product.nameLong, @@ -52,35 +77,42 @@ function buildWin32Setup(arch) { DirName: product.win32DirName, Version: pkg.version, RawVersion: pkg.version.replace(/-\w+$/, ''), - NameVersion: product.win32NameVersion, + NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''), ExeBasename: product.nameShort, RegValueName: product.win32RegValueName, ShellNameShort: product.win32ShellNameShort, AppMutex: product.win32MutexName, Arch: arch, AppId: arch === 'ia32' ? ia32AppId : x64AppId, - IncompatibleAppId: arch === 'ia32' ? x64AppId : ia32AppId, + IncompatibleTargetAppId: arch === 'ia32' ? product.win32AppId : product.win32x64AppId, + IncompatibleArchAppId: arch === 'ia32' ? x64AppId : ia32AppId, AppUserId: product.win32AppUserModelId, ArchitecturesAllowed: arch === 'ia32' ? '' : 'x64', ArchitecturesInstallIn64BitMode: arch === 'ia32' ? '' : 'x64', - SourceDir: buildPath(arch), + SourceDir: sourcePath, RepoDir: repoPath, - OutputDir: setupDir(arch) + OutputDir: outputPath, + InstallTarget: target, + ProductJsonPath: productJsonPath }; packageInnoSetup(issPath, { definitions }, cb); }; } -gulp.task('clean-vscode-win32-ia32-setup', util.rimraf(setupDir('ia32'))); -gulp.task('vscode-win32-ia32-setup', ['clean-vscode-win32-ia32-setup'], buildWin32Setup('ia32')); +function defineWin32SetupTasks(arch, target) { + gulp.task(`clean-vscode-win32-${arch}-${target}-setup`, util.rimraf(setupDir(arch, target))); + gulp.task(`vscode-win32-${arch}-${target}-setup`, [`clean-vscode-win32-${arch}-${target}-setup`], buildWin32Setup(arch, target)); +} -gulp.task('clean-vscode-win32-x64-setup', util.rimraf(setupDir('x64'))); -gulp.task('vscode-win32-x64-setup', ['clean-vscode-win32-x64-setup'], buildWin32Setup('x64')); +defineWin32SetupTasks('ia32', 'system'); +defineWin32SetupTasks('x64', 'system'); +defineWin32SetupTasks('ia32', 'user'); +defineWin32SetupTasks('x64', 'user'); function archiveWin32Setup(arch) { return cb => { - const args = ['a', '-tzip', zipPath(arch), '.', '-r']; + const args = ['a', '-tzip', zipPath(arch), '-x!CodeSignSummary*.md', '.', '-r']; cp.spawn(_7z, args, { stdio: 'inherit', cwd: buildPath(arch) }) .on('error', cb) diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index c03360cf002..88266f3b901 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -17,7 +17,6 @@ const ext = require('./extensions'); const util = require('gulp-util'); const root = path.dirname(path.dirname(__dirname)); -// @ts-ignore Microsoft/TypeScript#21262 complains about a require of a JSON file const builtInExtensions = require('../builtInExtensions.json'); const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index da38acec4f4..43855048c1f 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -46,7 +46,7 @@ export interface IEntryPoint { name: string; include?: string[]; exclude?: string[]; - prepend: string[]; + prepend?: string[]; append?: string[]; dest?: string; } diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 998ebb4f379..4f2f374bb5b 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -18,21 +18,24 @@ var _ = require("underscore"); var monacodts = require("../monaco/api"); var fs = require("fs"); var reporter = reporter_1.createReporter(); -var rootDir = path.join(__dirname, '../../src'); -var options = require('../../src/tsconfig.json').compilerOptions; -options.verbose = false; -options.sourceMap = true; -if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry - options.sourceMap = false; +function getTypeScriptCompilerOptions(src) { + var rootDir = path.join(__dirname, "../../" + src); + var options = require("../../" + src + "/tsconfig.json").compilerOptions; + options.verbose = false; + options.sourceMap = true; + if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry + options.sourceMap = false; + } + options.rootDir = rootDir; + options.sourceRoot = util.toFileUri(rootDir); + options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; + return options; } -options.rootDir = rootDir; -options.sourceRoot = util.toFileUri(rootDir); -options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; -function createCompile(build, emitError) { - var opts = _.clone(options); +function createCompile(src, build, emitError) { + var opts = _.clone(getTypeScriptCompilerOptions(src)); opts.inlineSources = !!build; opts.noFilesystemLookup = true; - var ts = tsb.create(opts, null, null, function (err) { return reporter(err.toString()); }); + var ts = tsb.create(opts, true, null, function (err) { return reporter(err.toString()); }); return function (token) { var utf8Filter = util.filter(function (data) { return /(\/|\\)test(\/|\\).*utf8/.test(data.path); }); var tsFilter = util.filter(function (data) { return /\.ts$/.test(data.path); }); @@ -51,32 +54,33 @@ function createCompile(build, emitError) { .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: opts.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); return es.duplex(input, output); }; } -function compileTask(out, build) { +var libDtsGlob = 'node_modules/typescript/lib/*.d.ts'; +function compileTask(src, out, build) { return function () { - var compile = createCompile(build, true); - var src = es.merge(gulp.src('src/**', { base: 'src' }), gulp.src('node_modules/typescript/lib/lib.d.ts')); + var compile = createCompile(src, build, true); + var srcPipe = es.merge(gulp.src(src + "/**", { base: "" + src }), gulp.src(libDtsGlob)); // Do not write .d.ts files to disk, as they are not needed there. var dtsFilter = util.filter(function (data) { return !/\.d\.ts$/.test(data.path); }); - return src + return srcPipe .pipe(compile()) .pipe(dtsFilter) .pipe(gulp.dest(out)) .pipe(dtsFilter.restore) - .pipe(monacodtsTask(out, false)); + .pipe(src !== 'src' ? es.through() : monacodtsTask(out, false)); }; } exports.compileTask = compileTask; function watchTask(out, build) { return function () { - var compile = createCompile(build); - var src = es.merge(gulp.src('src/**', { base: 'src' }), gulp.src('node_modules/typescript/lib/lib.d.ts')); + var compile = createCompile('src', build); + var src = es.merge(gulp.src('src/**', { base: 'src' }), gulp.src(libDtsGlob)); var watchSrc = watch('src/**', { base: 'src' }); // Do not write .d.ts files to disk, as they are not needed there. var dtsFilter = util.filter(function (data) { return !/\.d\.ts$/.test(data.path); }); @@ -122,6 +126,7 @@ function monacodtsTask(out, isWatch) { fs.writeFileSync(result.filePath, result.content); } else { + fs.writeFileSync(result.filePath, result.content); resultStream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); } } diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index cedcb4155b6..2f7dbe664bd 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -21,23 +21,26 @@ import * as fs from 'fs'; const reporter = createReporter(); -const rootDir = path.join(__dirname, '../../src'); -const options = require('../../src/tsconfig.json').compilerOptions; -options.verbose = false; -options.sourceMap = true; -if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry - options.sourceMap = false; +function getTypeScriptCompilerOptions(src: string) { + const rootDir = path.join(__dirname, `../../${src}`); + const options = require(`../../${src}/tsconfig.json`).compilerOptions; + options.verbose = false; + options.sourceMap = true; + if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry + options.sourceMap = false; + } + options.rootDir = rootDir; + options.sourceRoot = util.toFileUri(rootDir); + options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; + return options; } -options.rootDir = rootDir; -options.sourceRoot = util.toFileUri(rootDir); -options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 'CRLF' : 'LF'; -function createCompile(build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { - const opts = _.clone(options); +function createCompile(src: string, build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { + const opts = _.clone(getTypeScriptCompilerOptions(src)); opts.inlineSources = !!build; opts.noFilesystemLookup = true; - const ts = tsb.create(opts, null, null, err => reporter(err.toString())); + const ts = tsb.create(opts, true, null, err => reporter(err.toString())); return function (token?: util.ICancellationToken) { @@ -59,7 +62,7 @@ function createCompile(build: boolean, emitError?: boolean): (token?: util.ICanc .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: options.sourceRoot + sourceRoot: opts.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); @@ -68,36 +71,38 @@ function createCompile(build: boolean, emitError?: boolean): (token?: util.ICanc }; } -export function compileTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { +const libDtsGlob = 'node_modules/typescript/lib/*.d.ts'; + +export function compileTask(src: string, out: string, build: boolean): () => NodeJS.ReadWriteStream { return function () { - const compile = createCompile(build, true); + const compile = createCompile(src, build, true); - const src = es.merge( - gulp.src('src/**', { base: 'src' }), - gulp.src('node_modules/typescript/lib/lib.d.ts'), + const srcPipe = es.merge( + gulp.src(`${src}/**`, { base: `${src}` }), + gulp.src(libDtsGlob), ); // Do not write .d.ts files to disk, as they are not needed there. const dtsFilter = util.filter(data => !/\.d\.ts$/.test(data.path)); - return src + return srcPipe .pipe(compile()) .pipe(dtsFilter) .pipe(gulp.dest(out)) .pipe(dtsFilter.restore) - .pipe(monacodtsTask(out, false)); + .pipe(src !== 'src' ? es.through() : monacodtsTask(out, false)); }; } export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteStream { return function () { - const compile = createCompile(build); + const compile = createCompile('src', build); const src = es.merge( gulp.src('src/**', { base: 'src' }), - gulp.src('node_modules/typescript/lib/lib.d.ts'), + gulp.src(libDtsGlob), ); const watchSrc = watch('src/**', { base: 'src' }); @@ -150,6 +155,7 @@ function monacodtsTask(out: string, isWatch: boolean): NodeJS.ReadWriteStream { if (isWatch) { fs.writeFileSync(result.filePath, result.content); } else { + fs.writeFileSync(result.filePath, result.content); resultStream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); } } diff --git a/build/lib/extensions.js b/build/lib/extensions.js index ce74ac25ee6..e9ea4c6a8e9 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -3,8 +3,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; Object.defineProperty(exports, "__esModule", { value: true }); var es = require("event-stream"); +var fs = require("fs"); +var glob = require("glob"); +var gulp = require("gulp"); +var path = require("path"); +var File = require("vinyl"); +var vsce = require("vsce"); +var stats_1 = require("./stats"); +var util2 = require("./util"); var assign = require("object-assign"); var remote = require("gulp-remote-src"); var flatmap = require('gulp-flatmap'); @@ -14,11 +33,106 @@ var rename = require('gulp-rename'); var util = require('gulp-util'); var buffer = require('gulp-buffer'); var json = require('gulp-json-editor'); -var fs = require("fs"); -var path = require("path"); -var vsce = require("vsce"); -var File = require("vinyl"); -function fromLocal(extensionPath) { +var webpack = require('webpack'); +var webpackGulp = require('webpack-stream'); +var root = path.resolve(path.join(__dirname, '..', '..')); +function fromLocal(extensionPath, sourceMappingURLBase) { + var webpackFilename = path.join(extensionPath, 'extension.webpack.config.js'); + if (fs.existsSync(webpackFilename)) { + return fromLocalWebpack(extensionPath, sourceMappingURLBase); + } + else { + return fromLocalNormal(extensionPath); + } +} +exports.fromLocal = fromLocal; +function fromLocalWebpack(extensionPath, sourceMappingURLBase) { + var result = es.through(); + var packagedDependencies = []; + var packageJsonConfig = require(path.join(extensionPath, 'package.json')); + var webpackRootConfig = require(path.join(extensionPath, 'extension.webpack.config.js')); + for (var key in webpackRootConfig.externals) { + if (key in packageJsonConfig.dependencies) { + packagedDependencies.push(key); + } + } + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn, packagedDependencies: packagedDependencies }).then(function (fileNames) { + var files = fileNames + .map(function (fileName) { return path.join(extensionPath, fileName); }) + .map(function (filePath) { return new File({ + path: filePath, + stat: fs.statSync(filePath), + base: extensionPath, + contents: fs.createReadStream(filePath) + }); }); + var filesStream = es.readArray(files); + // check for a webpack configuration files, then invoke webpack + // and merge its output with the files stream. also rewrite the package.json + // file to a new entry point + var webpackConfigLocations = glob.sync(path.join(extensionPath, '/**/extension.webpack.config.js'), { ignore: ['**/node_modules'] }); + var packageJsonFilter = filter(function (f) { + if (path.basename(f.path) === 'package.json') { + // only modify package.json's next to the webpack file. + // to be safe, use existsSync instead of path comparison. + return fs.existsSync(path.join(path.dirname(f.path), 'extension.webpack.config.js')); + } + return false; + }, { restore: true }); + var patchFilesStream = filesStream + .pipe(packageJsonFilter) + .pipe(buffer()) + .pipe(json(function (data) { + // hardcoded entry point directory! + data.main = data.main.replace('/out/', /dist/); + return data; + })) + .pipe(packageJsonFilter.restore); + var webpackStreams = webpackConfigLocations.map(function (webpackConfigPath) { + var webpackDone = function (err, stats) { + util.log("Bundled extension: " + util.colors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath))) + "..."); + if (err) { + result.emit('error', err); + } + var compilation = stats.compilation; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('error', compilation.warnings.join('\n')); + } + }; + var webpackConfig = __assign({}, require(webpackConfigPath), { mode: 'production' }); + var relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + if (sourceMappingURLBase) { + var contents = data.contents.toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return "\n//# sourceMappingURL=" + sourceMappingURLBase + "/extensions/" + path.basename(extensionPath) + "/" + relativeOutputPath + "/" + g1; + }), 'utf8'); + if (/\.js\.map$/.test(data.path)) { + if (!fs.existsSync(path.dirname(data.path))) { + fs.mkdirSync(path.dirname(data.path)); + } + fs.writeFileSync(data.path, data.contents); + } + } + this.emit('data', data); + })); + }); + es.merge.apply(es, webpackStreams.concat([patchFilesStream])).pipe(result); + }).catch(function (err) { return result.emit('error', err); }); + return result.pipe(stats_1.createStatsStream(path.basename(extensionPath))); +} +function fromLocalNormal(extensionPath) { var result = es.through(); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn }) .then(function (fileNames) { @@ -33,9 +147,8 @@ function fromLocal(extensionPath) { es.readArray(files).pipe(result); }) .catch(function (err) { return result.emit('error', err); }); - return result; + return result.pipe(stats_1.createStatsStream(path.basename(extensionPath))); } -exports.fromLocal = fromLocal; function error(err) { var result = es.through(); setTimeout(function () { return result.emit('error', err); }); @@ -117,3 +230,49 @@ function fromMarketplace(extensionName, version) { })); } exports.fromMarketplace = fromMarketplace; +var excludedExtensions = [ + 'vscode-api-tests', + 'vscode-colorize-tests', + 'ms-vscode.node-debug', + 'ms-vscode.node-debug2', +]; +var builtInExtensions = require('../builtInExtensions.json'); +function packageExtensionsStream(opts) { + opts = opts || {}; + var localExtensionDescriptions = glob.sync('extensions/*/package.json') + .map(function (manifestPath) { + var extensionPath = path.dirname(path.join(root, manifestPath)); + var extensionName = path.basename(extensionPath); + return { name: extensionName, path: extensionPath }; + }) + .filter(function (_a) { + var name = _a.name; + return excludedExtensions.indexOf(name) === -1; + }) + .filter(function (_a) { + var name = _a.name; + return opts.desiredExtensions ? opts.desiredExtensions.indexOf(name) >= 0 : true; + }) + .filter(function (_a) { + var name = _a.name; + return builtInExtensions.every(function (b) { return b.name !== name; }); + }); + var localExtensions = es.merge.apply(es, localExtensionDescriptions.map(function (extension) { + return fromLocal(extension.path, opts.sourceMappingURLBase) + .pipe(rename(function (p) { return p.dirname = "extensions/" + extension.name + "/" + p.dirname; })); + })); + var localExtensionDependencies = gulp.src('extensions/node_modules/**', { base: '.' }); + var marketplaceExtensions = es.merge.apply(es, builtInExtensions + .filter(function (_a) { + var name = _a.name; + return opts.desiredExtensions ? opts.desiredExtensions.indexOf(name) >= 0 : true; + }) + .map(function (extension) { + return fromMarketplace(extension.name, extension.version) + .pipe(rename(function (p) { return p.dirname = "extensions/" + extension.name + "/" + p.dirname; })); + })); + return es.merge(localExtensions, localExtensionDependencies, marketplaceExtensions) + .pipe(util2.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.js.map'])); +} +exports.packageExtensionsStream = packageExtensionsStream; diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 4b05bea1979..731348763c1 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -4,7 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as es from 'event-stream'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as gulp from 'gulp'; +import * as path from 'path'; import { Stream } from 'stream'; +import * as File from 'vinyl'; +import * as vsce from 'vsce'; +import { createStatsStream } from './stats'; +import * as util2 from './util'; import assign = require('object-assign'); import remote = require('gulp-remote-src'); const flatmap = require('gulp-flatmap'); @@ -14,12 +22,135 @@ const rename = require('gulp-rename'); const util = require('gulp-util'); const buffer = require('gulp-buffer'); const json = require('gulp-json-editor'); -import * as fs from 'fs'; -import * as path from 'path'; -import * as vsce from 'vsce'; -import * as File from 'vinyl'; +const webpack = require('webpack'); +const webpackGulp = require('webpack-stream'); -export function fromLocal(extensionPath: string): Stream { +const root = path.resolve(path.join(__dirname, '..', '..')); + +export function fromLocal(extensionPath: string, sourceMappingURLBase?: string): Stream { + const webpackFilename = path.join(extensionPath, 'extension.webpack.config.js'); + if (fs.existsSync(webpackFilename)) { + return fromLocalWebpack(extensionPath, sourceMappingURLBase); + } else { + return fromLocalNormal(extensionPath); + } +} + +function fromLocalWebpack(extensionPath: string, sourceMappingURLBase: string): Stream { + let result = es.through(); + + let packagedDependencies: string[] = []; + let packageJsonConfig = require(path.join(extensionPath, 'package.json')); + let webpackRootConfig = require(path.join(extensionPath, 'extension.webpack.config.js')); + for (const key in webpackRootConfig.externals) { + if (key in packageJsonConfig.dependencies) { + packagedDependencies.push(key); + } + } + + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn, packagedDependencies }).then(fileNames => { + const files = fileNames + .map(fileName => path.join(extensionPath, fileName)) + .map(filePath => new File({ + path: filePath, + stat: fs.statSync(filePath), + base: extensionPath, + contents: fs.createReadStream(filePath) as any + })); + + const filesStream = es.readArray(files); + + // check for a webpack configuration files, then invoke webpack + // and merge its output with the files stream. also rewrite the package.json + // file to a new entry point + const webpackConfigLocations = (glob.sync( + path.join(extensionPath, '/**/extension.webpack.config.js'), + { ignore: ['**/node_modules'] } + )); + + const packageJsonFilter = filter(f => { + if (path.basename(f.path) === 'package.json') { + // only modify package.json's next to the webpack file. + // to be safe, use existsSync instead of path comparison. + return fs.existsSync(path.join(path.dirname(f.path), 'extension.webpack.config.js')); + } + return false; + }, { restore: true }); + + const patchFilesStream = filesStream + .pipe(packageJsonFilter) + .pipe(buffer()) + .pipe(json(data => { + // hardcoded entry point directory! + data.main = data.main.replace('/out/', /dist/); + return data; + })) + .pipe(packageJsonFilter.restore); + + + const webpackStreams = webpackConfigLocations.map(webpackConfigPath => { + + const webpackDone = (err, stats) => { + util.log(`Bundled extension: ${util.colors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + if (err) { + result.emit('error', err); + } + const { compilation } = stats; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('error', compilation.warnings.join('\n')); + } + }; + + const webpackConfig = { + ...require(webpackConfigPath), + ...{ mode: 'production' } + }; + let relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + if (sourceMappingURLBase) { + const contents = (data.contents).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + + if (/\.js\.map$/.test(data.path)) { + if (!fs.existsSync(path.dirname(data.path))) { + fs.mkdirSync(path.dirname(data.path)); + } + fs.writeFileSync(data.path, data.contents); + } + } + this.emit('data', data); + })); + }); + + es.merge(...webpackStreams, patchFilesStream) + // .pipe(es.through(function (data) { + // // debug + // console.log('out', data.path, data.contents.length); + // this.emit('data', data); + // })) + .pipe(result); + + }).catch(err => result.emit('error', err)); + + return result.pipe(createStatsStream(path.basename(extensionPath))); +} + +function fromLocalNormal(extensionPath: string): Stream { const result = es.through(); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn }) @@ -37,7 +168,7 @@ export function fromLocal(extensionPath: string): Stream { }) .catch(err => result.emit('error', err)); - return result; + return result.pipe(createStatsStream(path.basename(extensionPath))); } function error(err: any): Stream { @@ -131,3 +262,54 @@ export function fromMarketplace(extensionName: string, version: string): Stream })); })); } + +interface IPackageExtensionsOptions { + /** + * Set to undefined to package all of them. + */ + desiredExtensions?: string[]; + sourceMappingURLBase?: string; +} + +const excludedExtensions = [ + 'vscode-api-tests', + 'vscode-colorize-tests', + 'ms-vscode.node-debug', + 'ms-vscode.node-debug2', +]; + +const builtInExtensions: { name: string, version: string, repo: string; }[] = require('../builtInExtensions.json'); + +export function packageExtensionsStream(opts?: IPackageExtensionsOptions): NodeJS.ReadWriteStream { + opts = opts || {}; + + const localExtensionDescriptions = (glob.sync('extensions/*/package.json')) + .map(manifestPath => { + const extensionPath = path.dirname(path.join(root, manifestPath)); + const extensionName = path.basename(extensionPath); + return { name: extensionName, path: extensionPath }; + }) + .filter(({ name }) => excludedExtensions.indexOf(name) === -1) + .filter(({ name }) => opts.desiredExtensions ? opts.desiredExtensions.indexOf(name) >= 0 : true) + .filter(({ name }) => builtInExtensions.every(b => b.name !== name)); + + const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { + return fromLocal(extension.path, opts.sourceMappingURLBase) + .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + })); + + const localExtensionDependencies = gulp.src('extensions/node_modules/**', { base: '.' }); + + const marketplaceExtensions = es.merge( + ...builtInExtensions + .filter(({ name }) => opts.desiredExtensions ? opts.desiredExtensions.indexOf(name) >= 0 : true) + .map(extension => { + return fromMarketplace(extension.name, extension.version) + .pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); + }) + ); + + return es.merge(localExtensions, localExtensionDependencies, marketplaceExtensions) + .pipe(util2.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.js.map'])); +} diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 87da5e9bb0d..81fda4c4e9e 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -78,6 +78,10 @@ "name": "vs/workbench/parts/logs", "project": "vscode-workbench" }, + { + "name": "vs/workbench/parts/navigation", + "project": "vscode-workbench" + }, { "name": "vs/workbench/parts/output", "project": "vscode-workbench" diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 2693d9f519a..044780ba7fd 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -4,23 +4,24 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -var path = require("path"); +var es = require("event-stream"); var gulp = require("gulp"); -var sourcemaps = require("gulp-sourcemaps"); -var filter = require("gulp-filter"); +var concat = require("gulp-concat"); var minifyCSS = require("gulp-cssnano"); +var filter = require("gulp-filter"); +var flatmap = require("gulp-flatmap"); +var sourcemaps = require("gulp-sourcemaps"); var uglify = require("gulp-uglify"); var composer = require("gulp-uglify/composer"); +var gulpUtil = require("gulp-util"); +var path = require("path"); +var pump = require("pump"); var uglifyes = require("uglify-es"); -var es = require("event-stream"); -var concat = require("gulp-concat"); var VinylFile = require("vinyl"); var bundle = require("./bundle"); +var i18n_1 = require("./i18n"); +var stats_1 = require("./stats"); var util = require("./util"); -var i18n = require("./i18n"); -var gulpUtil = require("gulp-util"); -var flatmap = require("gulp-flatmap"); -var pump = require("pump"); var REPO_ROOT_PATH = path.join(__dirname, '../..'); function log(prefix, message) { gulpUtil.log(gulpUtil.colors.cyan('[' + prefix + ']'), message); @@ -38,19 +39,19 @@ function loaderConfig(emptyPaths) { } exports.loaderConfig = loaderConfig; var IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; -function loader(bundledFileHeader, bundleLoader) { +function loader(src, bundledFileHeader, bundleLoader) { var sources = [ - 'out-build/vs/loader.js' + src + "/vs/loader.js" ]; if (bundleLoader) { sources = sources.concat([ - 'out-build/vs/css.js', - 'out-build/vs/nls.js' + src + "/vs/css.js", + src + "/vs/nls.js" ]); } var isFirst = true; return (gulp - .src(sources, { base: 'out-build' }) + .src(sources, { base: "" + src }) .pipe(es.through(function (data) { if (isFirst) { isFirst = false; @@ -72,7 +73,7 @@ function loader(bundledFileHeader, bundleLoader) { return f; }))); } -function toConcatStream(bundledFileHeader, sources, dest) { +function toConcatStream(src, bundledFileHeader, sources, dest) { var useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then // insert a fake source at the beginning of each bundle with our copyright @@ -92,7 +93,7 @@ function toConcatStream(bundledFileHeader, sources, dest) { } var treatedSources = sources.map(function (source) { var root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; - var base = source.path ? root + '/out-build' : ''; + var base = source.path ? root + ("/" + src) : ''; return new VinylFile({ path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', base: base, @@ -101,14 +102,16 @@ function toConcatStream(bundledFileHeader, sources, dest) { }); return es.readArray(treatedSources) .pipe(useSourcemaps ? util.loadSourcemaps() : es.through()) - .pipe(concat(dest)); + .pipe(concat(dest)) + .pipe(stats_1.createStatsStream(dest)); } -function toBundleStream(bundledFileHeader, bundles) { +function toBundleStream(src, bundledFileHeader, bundles) { return es.merge(bundles.map(function (bundle) { - return toConcatStream(bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); })); } function optimizeTask(opts) { + var src = opts.src; var entryPoints = opts.entryPoints; var otherSources = opts.otherSources; var resources = opts.resources; @@ -124,7 +127,7 @@ function optimizeTask(opts) { if (err) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); // Remove css inlined resources var filteredResources = resources.slice(); result.cssInlinedResources.forEach(function (resource) { @@ -133,7 +136,7 @@ function optimizeTask(opts) { } filteredResources.push('!' + resource); }); - gulp.src(filteredResources, { base: 'out-build' }).pipe(resourcesStream); + gulp.src(filteredResources, { base: "" + src }).pipe(resourcesStream); var bundleInfoArray = []; if (opts.bundleInfo) { bundleInfoArray.push(new VinylFile({ @@ -146,9 +149,9 @@ function optimizeTask(opts) { }); var otherSourcesStream = es.through(); var otherSourcesStreamArr = []; - gulp.src(otherSources, { base: 'out-build' }) + gulp.src(otherSources, { base: "" + src }) .pipe(es.through(function (data) { - otherSourcesStreamArr.push(toConcatStream(bundledFileHeader, [data], data.relative)); + otherSourcesStreamArr.push(toConcatStream(src, bundledFileHeader, [data], data.relative)); }, function () { if (!otherSourcesStreamArr.length) { setTimeout(function () { otherSourcesStream.emit('end'); }, 0); @@ -157,17 +160,17 @@ function optimizeTask(opts) { es.merge(otherSourcesStreamArr).pipe(otherSourcesStream); } })); - var result = es.merge(loader(bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, bundleInfoStream); + var result = es.merge(loader(src, bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, bundleInfoStream); return result .pipe(sourcemaps.write('./', { sourceRoot: null, addComment: true, includeContent: true })) - .pipe(i18n.processNlsFiles({ + .pipe(opts.languages && opts.languages.length ? i18n_1.processNlsFiles({ fileHeader: bundledFileHeader, languages: opts.languages - })) + }) : es.through()) .pipe(gulp.dest(out)); }; } diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index b5636ffb8ef..8a35523dbe2 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -5,24 +5,25 @@ 'use strict'; -import * as path from 'path'; +import * as es from 'event-stream'; import * as gulp from 'gulp'; -import * as sourcemaps from 'gulp-sourcemaps'; -import * as filter from 'gulp-filter'; +import * as concat from 'gulp-concat'; import * as minifyCSS from 'gulp-cssnano'; +import * as filter from 'gulp-filter'; +import * as flatmap from 'gulp-flatmap'; +import * as sourcemaps from 'gulp-sourcemaps'; import * as uglify from 'gulp-uglify'; import * as composer from 'gulp-uglify/composer'; -import * as uglifyes from 'uglify-es'; -import * as es from 'event-stream'; -import * as concat from 'gulp-concat'; -import * as VinylFile from 'vinyl'; -import * as bundle from './bundle'; -import * as util from './util'; -import * as i18n from './i18n'; import * as gulpUtil from 'gulp-util'; -import * as flatmap from 'gulp-flatmap'; +import * as path from 'path'; import * as pump from 'pump'; import * as sm from 'source-map'; +import * as uglifyes from 'uglify-es'; +import * as VinylFile from 'vinyl'; +import * as bundle from './bundle'; +import { Language, processNlsFiles } from './i18n'; +import { createStatsStream } from './stats'; +import * as util from './util'; const REPO_ROOT_PATH = path.join(__dirname, '../..'); @@ -50,21 +51,21 @@ declare class FileSourceMap extends VinylFile { public sourceMap: sm.RawSourceMap; } -function loader(bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { +function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { let sources = [ - 'out-build/vs/loader.js' + `${src}/vs/loader.js` ]; if (bundleLoader) { sources = sources.concat([ - 'out-build/vs/css.js', - 'out-build/vs/nls.js' + `${src}/vs/css.js`, + `${src}/vs/nls.js` ]); } let isFirst = true; return ( gulp - .src(sources, { base: 'out-build' }) + .src(sources, { base: `${src}` }) .pipe(es.through(function (data) { if (isFirst) { isFirst = false; @@ -87,7 +88,7 @@ function loader(bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWr ); } -function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest: string): NodeJS.ReadWriteStream { +function toConcatStream(src: string, bundledFileHeader: string, sources: bundle.IFile[], dest: string): NodeJS.ReadWriteStream { const useSourcemaps = /\.js$/.test(dest) && !/\.nls\.js$/.test(dest); // If a bundle ends up including in any of the sources our copyright, then @@ -110,7 +111,7 @@ function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest const treatedSources = sources.map(function (source) { const root = source.path ? REPO_ROOT_PATH.replace(/\\/g, '/') : ''; - const base = source.path ? root + '/out-build' : ''; + const base = source.path ? root + `/${src}` : ''; return new VinylFile({ path: source.path ? root + '/' + source.path.replace(/\\/g, '/') : 'fake', @@ -121,16 +122,21 @@ function toConcatStream(bundledFileHeader: string, sources: bundle.IFile[], dest return es.readArray(treatedSources) .pipe(useSourcemaps ? util.loadSourcemaps() : es.through()) - .pipe(concat(dest)); + .pipe(concat(dest)) + .pipe(createStatsStream(dest)); } -function toBundleStream(bundledFileHeader: string, bundles: bundle.IConcatFile[]): NodeJS.ReadWriteStream { +function toBundleStream(src: string, bundledFileHeader: string, bundles: bundle.IConcatFile[]): NodeJS.ReadWriteStream { return es.merge(bundles.map(function (bundle) { - return toConcatStream(bundledFileHeader, bundle.sources, bundle.dest); + return toConcatStream(src, bundledFileHeader, bundle.sources, bundle.dest); })); } export interface IOptimizeTaskOpts { + /** + * The folder to read files from. + */ + src: string; /** * (for AMD files, will get bundled and get Copyright treatment) */ @@ -161,11 +167,13 @@ export interface IOptimizeTaskOpts { */ out: string; /** - * (languages to process) + * (out folder name) */ - languages: i18n.Language[]; + languages?: Language[]; } + export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStream { + const src = opts.src; const entryPoints = opts.entryPoints; const otherSources = opts.otherSources; const resources = opts.resources; @@ -182,7 +190,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr bundle.bundle(entryPoints, loaderConfig, function (err, result) { if (err) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(bundledFileHeader, result.files).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files).pipe(bundlesStream); // Remove css inlined resources const filteredResources = resources.slice(); @@ -192,7 +200,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr } filteredResources.push('!' + resource); }); - gulp.src(filteredResources, { base: 'out-build' }).pipe(resourcesStream); + gulp.src(filteredResources, { base: `${src}` }).pipe(resourcesStream); const bundleInfoArray: VinylFile[] = []; if (opts.bundleInfo) { @@ -208,9 +216,9 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr const otherSourcesStream = es.through(); const otherSourcesStreamArr: NodeJS.ReadWriteStream[] = []; - gulp.src(otherSources, { base: 'out-build' }) + gulp.src(otherSources, { base: `${src}` }) .pipe(es.through(function (data) { - otherSourcesStreamArr.push(toConcatStream(bundledFileHeader, [data], data.relative)); + otherSourcesStreamArr.push(toConcatStream(src, bundledFileHeader, [data], data.relative)); }, function () { if (!otherSourcesStreamArr.length) { setTimeout(function () { otherSourcesStream.emit('end'); }, 0); @@ -220,7 +228,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr })); const result = es.merge( - loader(bundledFileHeader, bundleLoader), + loader(src, bundledFileHeader, bundleLoader), bundlesStream, otherSourcesStream, resourcesStream, @@ -233,10 +241,10 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr addComment: true, includeContent: true })) - .pipe(i18n.processNlsFiles({ + .pipe(opts.languages && opts.languages.length ? processNlsFiles({ fileHeader: bundledFileHeader, languages: opts.languages - })) + }) : es.through()) .pipe(gulp.dest(out)); }; } diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 12511b01d36..3e2bacc25d5 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -7,171 +7,237 @@ Object.defineProperty(exports, "__esModule", { value: true }); var ts = require("typescript"); var fs = require("fs"); var path = require("path"); +var tss = require("./treeshaking"); var REPO_ROOT = path.join(__dirname, '../../'); var SRC_DIR = path.join(REPO_ROOT, 'src'); -var OUT_EDITOR = path.join(REPO_ROOT, 'out-editor'); -function createESMSourcesAndResources(options) { +var dirCache = {}; +function writeFile(filePath, contents) { + function ensureDirs(dirPath) { + if (dirCache[dirPath]) { + return; + } + dirCache[dirPath] = true; + ensureDirs(path.dirname(dirPath)); + if (fs.existsSync(dirPath)) { + return; + } + fs.mkdirSync(dirPath); + } + ensureDirs(path.dirname(filePath)); + fs.writeFileSync(filePath, contents); +} +function extractEditor(options) { + var result = tss.shake(options); + for (var fileName in result) { + if (result.hasOwnProperty(fileName)) { + writeFile(path.join(options.destRoot, fileName), result[fileName]); + } + } + var copied = {}; + var copyFile = function (fileName) { + if (copied[fileName]) { + return; + } + copied[fileName] = true; + var srcPath = path.join(options.sourcesRoot, fileName); + var dstPath = path.join(options.destRoot, fileName); + writeFile(dstPath, fs.readFileSync(srcPath)); + }; + var writeOutputFile = function (fileName, contents) { + writeFile(path.join(options.destRoot, fileName), contents); + }; + for (var fileName in result) { + if (result.hasOwnProperty(fileName)) { + var fileContents = result[fileName]; + var info = ts.preProcessFile(fileContents); + for (var i = info.importedFiles.length - 1; i >= 0; i--) { + var importedFileName = info.importedFiles[i].fileName; + var importedFilePath = void 0; + if (/^vs\/css!/.test(importedFileName)) { + importedFilePath = importedFileName.substr('vs/css!'.length) + '.css'; + } + else { + importedFilePath = importedFileName; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { + importedFilePath = path.join(path.dirname(fileName), importedFilePath); + } + if (/\.css$/.test(importedFilePath)) { + transportCSS(importedFilePath, copyFile, writeOutputFile); + } + else { + if (fs.existsSync(path.join(options.sourcesRoot, importedFilePath + '.js'))) { + copyFile(importedFilePath + '.js'); + } + } + } + } + } + var tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.json')).toString()); + tsConfig.compilerOptions.noUnusedLocals = false; + tsConfig.compilerOptions.preserveConstEnums = false; + tsConfig.compilerOptions.declaration = false; + writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); + [ + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/css.js', + 'vs/loader.js', + 'vs/monaco.d.ts', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/nls.js', + 'vs/nls.mock.ts', + 'typings/lib.ie11_safe_es6.d.ts', + 'typings/thenable.d.ts', + 'typings/es6-promise.d.ts', + 'typings/require.d.ts', + ].forEach(copyFile); +} +exports.extractEditor = extractEditor; +function createESMSourcesAndResources2(options) { + var SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); var OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); var OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); - var in_queue = Object.create(null); - var queue = []; - var enqueue = function (module) { - if (in_queue[module]) { - return; + var getDestAbsoluteFilePath = function (file) { + var dest = options.renames[file.replace(/\\/g, '/')] || file; + if (dest === 'tsconfig.json') { + return path.join(OUT_FOLDER, "../tsconfig.json"); } - in_queue[module] = true; - queue.push(module); + if (/\.ts$/.test(dest)) { + return path.join(OUT_FOLDER, dest); + } + return path.join(OUT_RESOURCES_FOLDER, dest); }; - var seenDir = {}; - var createDirectoryRecursive = function (dir) { - if (seenDir[dir]) { - return; + var allFiles = walkDirRecursive(SRC_FOLDER); + for (var i = 0; i < allFiles.length; i++) { + var file = allFiles[i]; + if (options.ignores.indexOf(file.replace(/\\/g, '/')) >= 0) { + continue; } - var lastSlash = dir.lastIndexOf('/'); - if (lastSlash === -1) { - lastSlash = dir.lastIndexOf('\\'); + if (file === 'tsconfig.json') { + var tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); + tsConfig.compilerOptions.moduleResolution = undefined; + tsConfig.compilerOptions.baseUrl = undefined; + tsConfig.compilerOptions.module = 'es6'; + tsConfig.compilerOptions.rootDir = 'src'; + tsConfig.compilerOptions.outDir = path.relative(path.dirname(OUT_FOLDER), OUT_RESOURCES_FOLDER); + write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); + continue; } - if (lastSlash !== -1) { - createDirectoryRecursive(dir.substring(0, lastSlash)); + if (/\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file)) { + // Transport the files directly + write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); + continue; } - seenDir[dir] = true; - try { - fs.mkdirSync(dir); - } - catch (err) { } - }; - seenDir[REPO_ROOT] = true; - var toggleComments = function (fileContents) { - var lines = fileContents.split(/\r\n|\r|\n/); - var mode = 0; - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (mode === 0) { - if (/\/\/ ESM-comment-begin/.test(line)) { - mode = 1; - continue; + if (/\.ts$/.test(file)) { + // Transform the .ts file + var fileContents = fs.readFileSync(path.join(SRC_FOLDER, file)).toString(); + var info = ts.preProcessFile(fileContents); + for (var i_1 = info.importedFiles.length - 1; i_1 >= 0; i_1--) { + var importedFilename = info.importedFiles[i_1].fileName; + var pos = info.importedFiles[i_1].pos; + var end = info.importedFiles[i_1].end; + var importedFilepath = void 0; + if (/^vs\/css!/.test(importedFilename)) { + importedFilepath = importedFilename.substr('vs/css!'.length) + '.css'; } - if (/\/\/ ESM-uncomment-begin/.test(line)) { - mode = 2; - continue; + else { + importedFilepath = importedFilename; } - continue; + if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) { + importedFilepath = path.join(path.dirname(file), importedFilepath); + } + var relativePath = void 0; + if (importedFilepath === path.dirname(file)) { + relativePath = '../' + path.basename(path.dirname(file)); + } + else if (importedFilepath === path.dirname(path.dirname(file))) { + relativePath = '../../' + path.basename(path.dirname(path.dirname(file))); + } + else { + relativePath = path.relative(path.dirname(file), importedFilepath); + } + if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) { + relativePath = './' + relativePath; + } + fileContents = (fileContents.substring(0, pos + 1) + + relativePath + + fileContents.substring(end + 1)); } - if (mode === 1) { - if (/\/\/ ESM-comment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = '// ' + line; - continue; + fileContents = fileContents.replace(/import ([a-zA-z0-9]+) = require\(('[^']+')\);/g, function (_, m1, m2) { + return "import * as " + m1 + " from " + m2 + ";"; + }); + write(getDestAbsoluteFilePath(file), fileContents); + continue; + } + console.log("UNKNOWN FILE: " + file); + } + function walkDirRecursive(dir) { + if (dir.charAt(dir.length - 1) !== '/' || dir.charAt(dir.length - 1) !== '\\') { + dir += '/'; + } + var result = []; + _walkDirRecursive(dir, result, dir.length); + return result; + } + function _walkDirRecursive(dir, result, trimPos) { + var files = fs.readdirSync(dir); + for (var i = 0; i < files.length; i++) { + var file = path.join(dir, files[i]); + if (fs.statSync(file).isDirectory()) { + _walkDirRecursive(file, result, trimPos); } - if (mode === 2) { - if (/\/\/ ESM-uncomment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { - return indent; - }); + else { + result.push(file.substr(trimPos)); } } - return lines.join('\n'); - }; - var write = function (filePath, contents) { - var absoluteFilePath; - if (/\.ts$/.test(filePath)) { - absoluteFilePath = path.join(OUT_FOLDER, filePath); - } - else { - absoluteFilePath = path.join(OUT_RESOURCES_FOLDER, filePath); - } - createDirectoryRecursive(path.dirname(absoluteFilePath)); - if (/(\.ts$)|(\.js$)/.test(filePath)) { + } + function write(absoluteFilePath, contents) { + if (/(\.ts$)|(\.js$)/.test(absoluteFilePath)) { contents = toggleComments(contents.toString()); } - fs.writeFileSync(absoluteFilePath, contents); - }; - options.entryPoints.forEach(function (entryPoint) { return enqueue(entryPoint); }); - while (queue.length > 0) { - var module_1 = queue.shift(); - if (transportCSS(options, module_1, enqueue, write)) { - continue; + writeFile(absoluteFilePath, contents); + function toggleComments(fileContents) { + var lines = fileContents.split(/\r\n|\r|\n/); + var mode = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (mode === 0) { + if (/\/\/ ESM-comment-begin/.test(line)) { + mode = 1; + continue; + } + if (/\/\/ ESM-uncomment-begin/.test(line)) { + mode = 2; + continue; + } + continue; + } + if (mode === 1) { + if (/\/\/ ESM-comment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = '// ' + line; + continue; + } + if (mode === 2) { + if (/\/\/ ESM-uncomment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { + return indent; + }); + } + } + return lines.join('\n'); } - if (transportResource(options, module_1, enqueue, write)) { - continue; - } - if (transportDTS(options, module_1, enqueue, write)) { - continue; - } - var filename = void 0; - if (options.redirects[module_1]) { - filename = path.join(SRC_DIR, options.redirects[module_1] + '.ts'); - } - else { - filename = path.join(SRC_DIR, module_1 + '.ts'); - } - var fileContents = fs.readFileSync(filename).toString(); - var info = ts.preProcessFile(fileContents); - for (var i = info.importedFiles.length - 1; i >= 0; i--) { - var importedFilename = info.importedFiles[i].fileName; - var pos = info.importedFiles[i].pos; - var end = info.importedFiles[i].end; - var importedFilepath = void 0; - if (/^vs\/css!/.test(importedFilename)) { - importedFilepath = importedFilename.substr('vs/css!'.length) + '.css'; - } - else { - importedFilepath = importedFilename; - } - if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) { - importedFilepath = path.join(path.dirname(module_1), importedFilepath); - } - enqueue(importedFilepath); - var relativePath = void 0; - if (importedFilepath === path.dirname(module_1)) { - relativePath = '../' + path.basename(path.dirname(module_1)); - } - else if (importedFilepath === path.dirname(path.dirname(module_1))) { - relativePath = '../../' + path.basename(path.dirname(path.dirname(module_1))); - } - else { - relativePath = path.relative(path.dirname(module_1), importedFilepath); - } - if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) { - relativePath = './' + relativePath; - } - fileContents = (fileContents.substring(0, pos + 1) - + relativePath - + fileContents.substring(end + 1)); - } - fileContents = fileContents.replace(/import ([a-zA-z0-9]+) = require\(('[^']+')\);/g, function (_, m1, m2) { - return "import * as " + m1 + " from " + m2 + ";"; - }); - fileContents = fileContents.replace(/Thenable/g, 'PromiseLike'); - write(module_1 + '.ts', fileContents); } - var esm_opts = { - "compilerOptions": { - "outDir": path.relative(path.dirname(OUT_FOLDER), OUT_RESOURCES_FOLDER), - "rootDir": "src", - "module": "es6", - "target": "es5", - "experimentalDecorators": true, - "lib": [ - "dom", - "es5", - "es2015.collection", - "es2015.promise" - ], - "types": [] - } - }; - fs.writeFileSync(path.join(path.dirname(OUT_FOLDER), 'tsconfig.json'), JSON.stringify(esm_opts, null, '\t')); - var monacodts = fs.readFileSync(path.join(SRC_DIR, 'vs/monaco.d.ts')).toString(); - fs.writeFileSync(path.join(OUT_FOLDER, 'vs/monaco.d.ts'), monacodts); } -exports.createESMSourcesAndResources = createESMSourcesAndResources; -function transportCSS(options, module, enqueue, write) { +exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; +function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; } @@ -179,10 +245,10 @@ function transportCSS(options, module, enqueue, write) { var fileContents = fs.readFileSync(filename).toString(); var inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 var inlineResourcesLimit = 300000; //3000; // see https://github.com/Microsoft/monaco-editor/issues/336 - var newContents = _rewriteOrInlineUrls(filename, fileContents, inlineResources === 'base64', inlineResourcesLimit); + var newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64', inlineResourcesLimit); write(module, newContents); return true; - function _rewriteOrInlineUrls(originalFileFSPath, contents, forceBase64, inlineByteLimit) { + function _rewriteOrInlineUrls(contents, forceBase64, inlineByteLimit) { return _replaceURL(contents, function (url) { var imagePath = path.join(path.dirname(module), url); var fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); @@ -239,27 +305,3 @@ function transportCSS(options, module, enqueue, write) { return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; } } -function transportResource(options, module, enqueue, write) { - if (!/\.svg/.test(module)) { - return false; - } - write(module, fs.readFileSync(path.join(SRC_DIR, module))); - return true; -} -function transportDTS(options, module, enqueue, write) { - if (options.redirects[module] && fs.existsSync(path.join(SRC_DIR, options.redirects[module] + '.ts'))) { - return false; - } - if (!fs.existsSync(path.join(SRC_DIR, module + '.d.ts'))) { - return false; - } - write(module + '.d.ts', fs.readFileSync(path.join(SRC_DIR, module + '.d.ts'))); - var filename; - if (options.redirects[module]) { - write(module + '.js', fs.readFileSync(path.join(SRC_DIR, options.redirects[module] + '.js'))); - } - else { - write(module + '.js', fs.readFileSync(path.join(SRC_DIR, module + '.js'))); - } - return true; -} diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index a402cf68405..621b4aea6d4 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -6,199 +6,273 @@ import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; +import * as tss from './treeshaking'; const REPO_ROOT = path.join(__dirname, '../../'); const SRC_DIR = path.join(REPO_ROOT, 'src'); -const OUT_EDITOR = path.join(REPO_ROOT, 'out-editor'); -export interface IOptions { - entryPoints: string[]; - outFolder: string; - outResourcesFolder: string; - redirects: { [module: string]: string; }; +let dirCache: { [dir: string]: boolean; } = {}; + +function writeFile(filePath: string, contents: Buffer | string): void { + function ensureDirs(dirPath: string): void { + if (dirCache[dirPath]) { + return; + } + dirCache[dirPath] = true; + + ensureDirs(path.dirname(dirPath)); + if (fs.existsSync(dirPath)) { + return; + } + fs.mkdirSync(dirPath); + } + ensureDirs(path.dirname(filePath)); + fs.writeFileSync(filePath, contents); } -export function createESMSourcesAndResources(options: IOptions): void { +export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string }): void { + let result = tss.shake(options); + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + writeFile(path.join(options.destRoot, fileName), result[fileName]); + } + } + let copied: { [fileName: string]: boolean; } = {}; + const copyFile = (fileName: string) => { + if (copied[fileName]) { + return; + } + copied[fileName] = true; + const srcPath = path.join(options.sourcesRoot, fileName); + const dstPath = path.join(options.destRoot, fileName); + writeFile(dstPath, fs.readFileSync(srcPath)); + }; + const writeOutputFile = (fileName: string, contents: string) => { + writeFile(path.join(options.destRoot, fileName), contents); + }; + for (let fileName in result) { + if (result.hasOwnProperty(fileName)) { + const fileContents = result[fileName]; + const info = ts.preProcessFile(fileContents); + + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + + let importedFilePath: string; + if (/^vs\/css!/.test(importedFileName)) { + importedFilePath = importedFileName.substr('vs/css!'.length) + '.css'; + } else { + importedFilePath = importedFileName; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { + importedFilePath = path.join(path.dirname(fileName), importedFilePath); + } + + if (/\.css$/.test(importedFilePath)) { + transportCSS(importedFilePath, copyFile, writeOutputFile); + } else { + if (fs.existsSync(path.join(options.sourcesRoot, importedFilePath + '.js'))) { + copyFile(importedFilePath + '.js'); + } + } + } + } + } + + const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.json')).toString()); + tsConfig.compilerOptions.noUnusedLocals = false; + tsConfig.compilerOptions.preserveConstEnums = false; + tsConfig.compilerOptions.declaration = false; + writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); + + [ + 'vs/css.build.js', + 'vs/css.d.ts', + 'vs/css.js', + 'vs/loader.js', + 'vs/monaco.d.ts', + 'vs/nls.build.js', + 'vs/nls.d.ts', + 'vs/nls.js', + 'vs/nls.mock.ts', + 'typings/lib.ie11_safe_es6.d.ts', + 'typings/thenable.d.ts', + 'typings/es6-promise.d.ts', + 'typings/require.d.ts', + ].forEach(copyFile); +} + +export interface IOptions2 { + srcFolder: string; + outFolder: string; + outResourcesFolder: string; + ignores: string[]; + renames: { [filename: string]: string; }; +} + +export function createESMSourcesAndResources2(options: IOptions2): void { + const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); - let in_queue: { [module: string]: boolean; } = Object.create(null); - let queue: string[] = []; - - const enqueue = (module: string) => { - if (in_queue[module]) { - return; + const getDestAbsoluteFilePath = (file: string): string => { + let dest = options.renames[file.replace(/\\/g, '/')] || file; + if (dest === 'tsconfig.json') { + return path.join(OUT_FOLDER, `../tsconfig.json`); } - in_queue[module] = true; - queue.push(module); + if (/\.ts$/.test(dest)) { + return path.join(OUT_FOLDER, dest); + } + return path.join(OUT_RESOURCES_FOLDER, dest); }; - const seenDir: { [key: string]: boolean; } = {}; - const createDirectoryRecursive = (dir: string) => { - if (seenDir[dir]) { - return; - } + const allFiles = walkDirRecursive(SRC_FOLDER); + for (let i = 0; i < allFiles.length; i++) { + const file = allFiles[i]; - let lastSlash = dir.lastIndexOf('/'); - if (lastSlash === -1) { - lastSlash = dir.lastIndexOf('\\'); - } - if (lastSlash !== -1) { - createDirectoryRecursive(dir.substring(0, lastSlash)); - } - seenDir[dir] = true; - try { fs.mkdirSync(dir); } catch (err) { } - }; - - seenDir[REPO_ROOT] = true; - - const toggleComments = (fileContents: string) => { - let lines = fileContents.split(/\r\n|\r|\n/); - let mode = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (mode === 0) { - if (/\/\/ ESM-comment-begin/.test(line)) { - mode = 1; - continue; - } - if (/\/\/ ESM-uncomment-begin/.test(line)) { - mode = 2; - continue; - } - continue; - } - - if (mode === 1) { - if (/\/\/ ESM-comment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = '// ' + line; - continue; - } - - if (mode === 2) { - if (/\/\/ ESM-uncomment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { - return indent; - }); - } - } - - return lines.join('\n'); - }; - - const write = (filePath: string, contents: string | Buffer) => { - let absoluteFilePath: string; - if (/\.ts$/.test(filePath)) { - absoluteFilePath = path.join(OUT_FOLDER, filePath); - } else { - absoluteFilePath = path.join(OUT_RESOURCES_FOLDER, filePath); - } - createDirectoryRecursive(path.dirname(absoluteFilePath)); - if (/(\.ts$)|(\.js$)/.test(filePath)) { - contents = toggleComments(contents.toString()); - } - fs.writeFileSync(absoluteFilePath, contents); - }; - - options.entryPoints.forEach((entryPoint) => enqueue(entryPoint)); - - while (queue.length > 0) { - const module = queue.shift(); - if (transportCSS(options, module, enqueue, write)) { - continue; - } - if (transportResource(options, module, enqueue, write)) { - continue; - } - if (transportDTS(options, module, enqueue, write)) { + if (options.ignores.indexOf(file.replace(/\\/g, '/')) >= 0) { continue; } - let filename: string; - if (options.redirects[module]) { - filename = path.join(SRC_DIR, options.redirects[module] + '.ts'); - } else { - filename = path.join(SRC_DIR, module + '.ts'); - } - let fileContents = fs.readFileSync(filename).toString(); - - const info = ts.preProcessFile(fileContents); - - for (let i = info.importedFiles.length - 1; i >= 0; i--) { - const importedFilename = info.importedFiles[i].fileName; - const pos = info.importedFiles[i].pos; - const end = info.importedFiles[i].end; - - let importedFilepath: string; - if (/^vs\/css!/.test(importedFilename)) { - importedFilepath = importedFilename.substr('vs/css!'.length) + '.css'; - } else { - importedFilepath = importedFilename; - } - if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) { - importedFilepath = path.join(path.dirname(module), importedFilepath); - } - - enqueue(importedFilepath); - - let relativePath: string; - if (importedFilepath === path.dirname(module)) { - relativePath = '../' + path.basename(path.dirname(module)); - } else if (importedFilepath === path.dirname(path.dirname(module))) { - relativePath = '../../' + path.basename(path.dirname(path.dirname(module))); - } else { - relativePath = path.relative(path.dirname(module), importedFilepath); - } - if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) { - relativePath = './' + relativePath; - } - fileContents = ( - fileContents.substring(0, pos + 1) - + relativePath - + fileContents.substring(end + 1) - ); + if (file === 'tsconfig.json') { + const tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); + tsConfig.compilerOptions.moduleResolution = undefined; + tsConfig.compilerOptions.baseUrl = undefined; + tsConfig.compilerOptions.module = 'es6'; + tsConfig.compilerOptions.rootDir = 'src'; + tsConfig.compilerOptions.outDir = path.relative(path.dirname(OUT_FOLDER), OUT_RESOURCES_FOLDER); + write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); + continue; } - fileContents = fileContents.replace(/import ([a-zA-z0-9]+) = require\(('[^']+')\);/g, function (_, m1, m2) { - return `import * as ${m1} from ${m2};`; - }); - fileContents = fileContents.replace(/Thenable/g, 'PromiseLike'); + if (/\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file)) { + // Transport the files directly + write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); + continue; + } - write(module + '.ts', fileContents); + if (/\.ts$/.test(file)) { + // Transform the .ts file + let fileContents = fs.readFileSync(path.join(SRC_FOLDER, file)).toString(); + + const info = ts.preProcessFile(fileContents); + + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFilename = info.importedFiles[i].fileName; + const pos = info.importedFiles[i].pos; + const end = info.importedFiles[i].end; + + let importedFilepath: string; + if (/^vs\/css!/.test(importedFilename)) { + importedFilepath = importedFilename.substr('vs/css!'.length) + '.css'; + } else { + importedFilepath = importedFilename; + } + if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) { + importedFilepath = path.join(path.dirname(file), importedFilepath); + } + + let relativePath: string; + if (importedFilepath === path.dirname(file)) { + relativePath = '../' + path.basename(path.dirname(file)); + } else if (importedFilepath === path.dirname(path.dirname(file))) { + relativePath = '../../' + path.basename(path.dirname(path.dirname(file))); + } else { + relativePath = path.relative(path.dirname(file), importedFilepath); + } + if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) { + relativePath = './' + relativePath; + } + fileContents = ( + fileContents.substring(0, pos + 1) + + relativePath + + fileContents.substring(end + 1) + ); + } + + fileContents = fileContents.replace(/import ([a-zA-z0-9]+) = require\(('[^']+')\);/g, function (_, m1, m2) { + return `import * as ${m1} from ${m2};`; + }); + + write(getDestAbsoluteFilePath(file), fileContents); + continue; + } + + console.log(`UNKNOWN FILE: ${file}`); } - const esm_opts = { - "compilerOptions": { - "outDir": path.relative(path.dirname(OUT_FOLDER), OUT_RESOURCES_FOLDER), - "rootDir": "src", - "module": "es6", - "target": "es5", - "experimentalDecorators": true, - "lib": [ - "dom", - "es5", - "es2015.collection", - "es2015.promise" - ], - "types": [ - ] + + function walkDirRecursive(dir: string): string[] { + if (dir.charAt(dir.length - 1) !== '/' || dir.charAt(dir.length - 1) !== '\\') { + dir += '/'; } - }; - fs.writeFileSync(path.join(path.dirname(OUT_FOLDER), 'tsconfig.json'), JSON.stringify(esm_opts, null, '\t')); + let result: string[] = []; + _walkDirRecursive(dir, result, dir.length); + return result; + } - const monacodts = fs.readFileSync(path.join(SRC_DIR, 'vs/monaco.d.ts')).toString(); - fs.writeFileSync(path.join(OUT_FOLDER, 'vs/monaco.d.ts'), monacodts); + function _walkDirRecursive(dir: string, result: string[], trimPos: number): void { + const files = fs.readdirSync(dir); + for (let i = 0; i < files.length; i++) { + const file = path.join(dir, files[i]); + if (fs.statSync(file).isDirectory()) { + _walkDirRecursive(file, result, trimPos); + } else { + result.push(file.substr(trimPos)); + } + } + } + function write(absoluteFilePath: string, contents: string | Buffer): void { + if (/(\.ts$)|(\.js$)/.test(absoluteFilePath)) { + contents = toggleComments(contents.toString()); + } + writeFile(absoluteFilePath, contents); + + function toggleComments(fileContents: string): string { + let lines = fileContents.split(/\r\n|\r|\n/); + let mode = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (mode === 0) { + if (/\/\/ ESM-comment-begin/.test(line)) { + mode = 1; + continue; + } + if (/\/\/ ESM-uncomment-begin/.test(line)) { + mode = 2; + continue; + } + continue; + } + + if (mode === 1) { + if (/\/\/ ESM-comment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = '// ' + line; + continue; + } + + if (mode === 2) { + if (/\/\/ ESM-uncomment-end/.test(line)) { + mode = 0; + continue; + } + lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { + return indent; + }); + } + } + + return lines.join('\n'); + } + } } -function transportCSS(options: IOptions, module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { +function transportCSS(module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { if (!/\.css/.test(module)) { return false; @@ -209,11 +283,11 @@ function transportCSS(options: IOptions, module: string, enqueue: (module: strin const inlineResources = 'base64'; // see https://github.com/Microsoft/monaco-editor/issues/148 const inlineResourcesLimit = 300000;//3000; // see https://github.com/Microsoft/monaco-editor/issues/336 - const newContents = _rewriteOrInlineUrls(filename, fileContents, inlineResources === 'base64', inlineResourcesLimit); + const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64', inlineResourcesLimit); write(module, newContents); return true; - function _rewriteOrInlineUrls(originalFileFSPath: string, contents: string, forceBase64: boolean, inlineByteLimit: number): string { + function _rewriteOrInlineUrls(contents: string, forceBase64: boolean, inlineByteLimit: number): string { return _replaceURL(contents, (url) => { let imagePath = path.join(path.dirname(module), url); let fileContents = fs.readFileSync(path.join(SRC_DIR, imagePath)); @@ -273,33 +347,3 @@ function transportCSS(options: IOptions, module: string, enqueue: (module: strin return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; } } - -function transportResource(options: IOptions, module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { - - if (!/\.svg/.test(module)) { - return false; - } - - write(module, fs.readFileSync(path.join(SRC_DIR, module))); - return true; -} - -function transportDTS(options: IOptions, module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { - - if (options.redirects[module] && fs.existsSync(path.join(SRC_DIR, options.redirects[module] + '.ts'))) { - return false; - } - - if (!fs.existsSync(path.join(SRC_DIR, module + '.d.ts'))) { - return false; - } - - write(module + '.d.ts', fs.readFileSync(path.join(SRC_DIR, module + '.d.ts'))); - let filename: string; - if (options.redirects[module]) { - write(module + '.js', fs.readFileSync(path.join(SRC_DIR, options.redirects[module] + '.js'))); - } else { - write(module + '.js', fs.readFileSync(path.join(SRC_DIR, module + '.js'))); - } - return true; -} diff --git a/build/lib/stats.js b/build/lib/stats.js new file mode 100644 index 00000000000..8659dc67b09 --- /dev/null +++ b/build/lib/stats.js @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +var es = require("event-stream"); +var util = require("gulp-util"); +var appInsights = require("applicationinsights"); +var Entry = /** @class */ (function () { + function Entry(name, totalCount, totalSize) { + this.name = name; + this.totalCount = totalCount; + this.totalSize = totalSize; + } + Entry.prototype.toString = function (pretty) { + if (!pretty) { + if (this.totalCount === 1) { + return this.name + ": " + this.totalSize + " bytes"; + } + else { + return this.name + ": " + this.totalCount + " files with " + this.totalSize + " bytes"; + } + } + else { + if (this.totalCount === 1) { + return "Stats for '" + util.colors.grey(this.name) + "': " + Math.round(this.totalSize / 1204) + "KB"; + } + else { + var count = this.totalCount < 100 + ? util.colors.green(this.totalCount.toString()) + : util.colors.red(this.totalCount.toString()); + return "Stats for '" + util.colors.grey(this.name) + "': " + count + " files, " + Math.round(this.totalSize / 1204) + "KB"; + } + } + }; + return Entry; +}()); +var _entries = new Map(); +function createStatsStream(group, log) { + var entry = new Entry(group, 0, 0); + _entries.set(entry.name, entry); + return es.through(function (data) { + var file = data; + if (typeof file.path === 'string') { + entry.totalCount += 1; + if (Buffer.isBuffer(file.contents)) { + entry.totalSize += file.contents.length; + } + else if (file.stat && typeof file.stat.size === 'number') { + entry.totalSize += file.stat.size; + } + else { + // funky file... + } + } + this.emit('data', data); + }, function () { + if (log) { + if (entry.totalCount === 1) { + util.log("Stats for '" + util.colors.grey(entry.name) + "': " + Math.round(entry.totalSize / 1204) + "KB"); + } + else { + var count = entry.totalCount < 100 + ? util.colors.green(entry.totalCount.toString()) + : util.colors.red(entry.totalCount.toString()); + util.log("Stats for '" + util.colors.grey(entry.name) + "': " + count + " files, " + Math.round(entry.totalSize / 1204) + "KB"); + } + } + this.emit('end'); + }); +} +exports.createStatsStream = createStatsStream; +function submitAllStats(productJson, commit) { + var sorted = []; + // move entries for single files to the front + _entries.forEach(function (value) { + if (value.totalCount === 1) { + sorted.unshift(value); + } + else { + sorted.push(value); + } + }); + // print to console + for (var _i = 0, sorted_1 = sorted; _i < sorted_1.length; _i++) { + var entry = sorted_1[_i]; + console.log(entry.toString(true)); + } + // send data as telementry event when the + // product is configured to send telemetry + if (!productJson || !productJson.aiConfig || typeof productJson.aiConfig.asimovKey !== 'string') { + return Promise.resolve(); + } + return new Promise(function (resolve) { + var sizes = {}; + var counts = {}; + for (var _i = 0, sorted_2 = sorted; _i < sorted_2.length; _i++) { + var entry = sorted_2[_i]; + sizes[entry.name] = entry.totalSize; + counts[entry.name] = entry.totalCount; + } + appInsights.setup(productJson.aiConfig.asimovKey) + .setAutoCollectConsole(false) + .setAutoCollectExceptions(false) + .setAutoCollectPerformance(false) + .setAutoCollectRequests(false) + .start(); + var client = appInsights.getClient(productJson.aiConfig.asimovKey); + client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + /* __GDPR__ + "monacoworkbench/packagemetrics" : { + "commit" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "size" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + "count" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } + */ + client.trackEvent("monacoworkbench/packagemetrics", { commit: commit, size: JSON.stringify(sizes), count: JSON.stringify(counts) }); + client.sendPendingData(function () { return resolve(); }); + }); +} +exports.submitAllStats = submitAllStats; diff --git a/build/lib/stats.ts b/build/lib/stats.ts new file mode 100644 index 00000000000..0d94fc31a0c --- /dev/null +++ b/build/lib/stats.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as es from 'event-stream'; +import * as util from 'gulp-util'; +import * as File from 'vinyl'; +import * as appInsights from 'applicationinsights'; + +class Entry { + constructor(readonly name: string, public totalCount: number, public totalSize: number) { } + + toString(pretty?: boolean): string { + if (!pretty) { + if (this.totalCount === 1) { + return `${this.name}: ${this.totalSize} bytes`; + } else { + return `${this.name}: ${this.totalCount} files with ${this.totalSize} bytes`; + } + } else { + if (this.totalCount === 1) { + return `Stats for '${util.colors.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; + + } else { + let count = this.totalCount < 100 + ? util.colors.green(this.totalCount.toString()) + : util.colors.red(this.totalCount.toString()); + + return `Stats for '${util.colors.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; + } + } + } +} + +const _entries = new Map(); + +export function createStatsStream(group: string, log?: boolean): es.ThroughStream { + + const entry = new Entry(group, 0, 0); + _entries.set(entry.name, entry); + + return es.through(function (data) { + let file = data as File; + if (typeof file.path === 'string') { + entry.totalCount += 1; + if (Buffer.isBuffer(file.contents)) { + entry.totalSize += file.contents.length; + } else if (file.stat && typeof file.stat.size === 'number') { + entry.totalSize += file.stat.size; + } else { + // funky file... + } + } + this.emit('data', data); + }, function () { + if (log) { + if (entry.totalCount === 1) { + util.log(`Stats for '${util.colors.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); + + } else { + let count = entry.totalCount < 100 + ? util.colors.green(entry.totalCount.toString()) + : util.colors.red(entry.totalCount.toString()); + + util.log(`Stats for '${util.colors.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); + } + } + + this.emit('end'); + }); +} + +export function submitAllStats(productJson: any, commit: string): Promise { + + let sorted: Entry[] = []; + // move entries for single files to the front + _entries.forEach(value => { + if (value.totalCount === 1) { + sorted.unshift(value); + } else { + sorted.push(value); + } + }); + + // print to console + for (const entry of sorted) { + console.log(entry.toString(true)); + } + + // send data as telementry event when the + // product is configured to send telemetry + if (!productJson || !productJson.aiConfig || typeof productJson.aiConfig.asimovKey !== 'string') { + return Promise.resolve(); + } + + return new Promise(resolve => { + + const sizes = {}; + const counts = {}; + for (const entry of sorted) { + sizes[entry.name] = entry.totalSize; + counts[entry.name] = entry.totalCount; + } + + appInsights.setup(productJson.aiConfig.asimovKey) + .setAutoCollectConsole(false) + .setAutoCollectExceptions(false) + .setAutoCollectPerformance(false) + .setAutoCollectRequests(false) + .start(); + + const client = appInsights.getClient(productJson.aiConfig.asimovKey); + client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + + /* __GDPR__ + "monacoworkbench/packagemetrics" : { + "commit" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "size" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + "count" : {"classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } + */ + client.trackEvent(`monacoworkbench/packagemetrics`, { commit, size: JSON.stringify(sizes), count: JSON.stringify(counts) }); + client.sendPendingData(() => resolve()); + }); + +} diff --git a/build/lib/test/util.test.js b/build/lib/test/util.test.js deleted file mode 100644 index ef0616173b6..00000000000 --- a/build/lib/test/util.test.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -var assert = require("assert"); -var util = require("../util"); -function getMockTagExists(tags) { - return function (tag) { return tags.indexOf(tag) >= 0; }; -} -suite('util tests', function () { - test('getPreviousVersion - patch', function () { - assert.equal(util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), '1.2.2'); - }); - test('getPreviousVersion - patch invalid', function () { - try { - util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); - test('getPreviousVersion - minor', function () { - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), '1.1.3'); - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), '1.1.0'); - }); - test('getPreviousVersion - minor gap', function () { - assert.equal(util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), '1.1.1'); - }); - test('getPreviousVersion - minor invalid', function () { - try { - util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); - test('getPreviousVersion - major', function () { - assert.equal(util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), '1.2.2'); - }); - test('getPreviousVersion - major invalid', function () { - try { - util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); - } - catch (e) { - // expected - return; - } - throw new Error('Expected an exception'); - }); -}); diff --git a/build/lib/test/util.test.ts b/build/lib/test/util.test.ts deleted file mode 100644 index 928e730f06c..00000000000 --- a/build/lib/test/util.test.ts +++ /dev/null @@ -1,79 +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 assert = require('assert'); -import util = require('../util'); - -function getMockTagExists(tags: string[]) { - return (tag: string) => tags.indexOf(tag) >= 0; -} - -suite('util tests', () => { - test('getPreviousVersion - patch', () => { - assert.equal( - util.getPreviousVersion('1.2.3', getMockTagExists(['1.2.2', '1.2.1', '1.2.0', '1.1.0'])), - '1.2.2' - ); - }); - - test('getPreviousVersion - patch invalid', () => { - try { - util.getPreviousVersion('1.2.2', getMockTagExists(['1.2.0', '1.1.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); - - test('getPreviousVersion - minor', () => { - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.2', '1.1.3'])), - '1.1.3' - ); - - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.0.0'])), - '1.1.0' - ); - }); - - test('getPreviousVersion - minor gap', () => { - assert.equal( - util.getPreviousVersion('1.2.0', getMockTagExists(['1.1.0', '1.1.1', '1.1.3'])), - '1.1.1' - ); - }); - - test('getPreviousVersion - minor invalid', () => { - try { - util.getPreviousVersion('1.2.0', getMockTagExists(['1.0.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); - - test('getPreviousVersion - major', () => { - assert.equal( - util.getPreviousVersion('2.0.0', getMockTagExists(['1.0.0', '1.1.0', '1.2.0', '1.2.1', '1.2.2'])), - '1.2.2' - ); - }); - - test('getPreviousVersion - major invalid', () => { - try { - util.getPreviousVersion('3.0.0', getMockTagExists(['1.0.0'])); - } catch (e) { - // expected - return; - } - - throw new Error('Expected an exception'); - }); -}); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js new file mode 100644 index 00000000000..c77fa128912 --- /dev/null +++ b/build/lib/treeshaking.js @@ -0,0 +1,682 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +var fs = require("fs"); +var path = require("path"); +var ts = require("typescript"); +var TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); +var ShakeLevel; +(function (ShakeLevel) { + ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; + ShakeLevel[ShakeLevel["InnerFile"] = 1] = "InnerFile"; + ShakeLevel[ShakeLevel["ClassMembers"] = 2] = "ClassMembers"; +})(ShakeLevel = exports.ShakeLevel || (exports.ShakeLevel = {})); +function shake(options) { + var languageService = createTypeScriptLanguageService(options); + markNodes(languageService, options); + return generateResult(languageService, options.shakeLevel); +} +exports.shake = shake; +//#region Discovery, LanguageService & Setup +function createTypeScriptLanguageService(options) { + // Discover referenced files + var FILES = discoverAndReadFiles(options); + // Add fake usage files + options.inlineEntryPoints.forEach(function (inlineEntryPoint, index) { + FILES["inlineEntryPoint:" + index + ".ts"] = inlineEntryPoint; + }); + // Resolve libs + var RESOLVED_LIBS = {}; + options.libs.forEach(function (filename) { + var filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + RESOLVED_LIBS["defaultLib:" + filename] = fs.readFileSync(filepath).toString(); + }); + var host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, options.compilerOptions); + return ts.createLanguageService(host); +} +/** + * Read imports and follow them until all files have been handled + */ +function discoverAndReadFiles(options) { + var FILES = {}; + var in_queue = Object.create(null); + var queue = []; + var enqueue = function (moduleId) { + if (in_queue[moduleId]) { + return; + } + in_queue[moduleId] = true; + queue.push(moduleId); + }; + options.entryPoints.forEach(function (entryPoint) { return enqueue(entryPoint); }); + while (queue.length > 0) { + var moduleId = queue.shift(); + var dts_filename = path.join(options.sourcesRoot, moduleId + '.d.ts'); + if (fs.existsSync(dts_filename)) { + var dts_filecontents = fs.readFileSync(dts_filename).toString(); + FILES[moduleId + '.d.ts'] = dts_filecontents; + continue; + } + var ts_filename = void 0; + if (options.redirects[moduleId]) { + ts_filename = path.join(options.sourcesRoot, options.redirects[moduleId] + '.ts'); + } + else { + ts_filename = path.join(options.sourcesRoot, moduleId + '.ts'); + } + var ts_filecontents = fs.readFileSync(ts_filename).toString(); + var info = ts.preProcessFile(ts_filecontents); + for (var i = info.importedFiles.length - 1; i >= 0; i--) { + var importedFileName = info.importedFiles[i].fileName; + if (options.importIgnorePattern.test(importedFileName)) { + // Ignore vs/css! imports + continue; + } + var importedModuleId = importedFileName; + if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { + importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + } + enqueue(importedModuleId); + } + FILES[moduleId + '.ts'] = ts_filecontents; + } + return FILES; +} +/** + * A TypeScript language service host + */ +var TypeScriptLanguageServiceHost = /** @class */ (function () { + function TypeScriptLanguageServiceHost(libs, files, compilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + // --- language service host --------------- + TypeScriptLanguageServiceHost.prototype.getCompilationSettings = function () { + return this._compilerOptions; + }; + TypeScriptLanguageServiceHost.prototype.getScriptFileNames = function () { + return ([] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files))); + }; + TypeScriptLanguageServiceHost.prototype.getScriptVersion = function (fileName) { + return '1'; + }; + TypeScriptLanguageServiceHost.prototype.getProjectVersion = function () { + return '1'; + }; + TypeScriptLanguageServiceHost.prototype.getScriptSnapshot = function (fileName) { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } + else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } + else { + return ts.ScriptSnapshot.fromString(''); + } + }; + TypeScriptLanguageServiceHost.prototype.getScriptKind = function (fileName) { + return ts.ScriptKind.TS; + }; + TypeScriptLanguageServiceHost.prototype.getCurrentDirectory = function () { + return ''; + }; + TypeScriptLanguageServiceHost.prototype.getDefaultLibFileName = function (options) { + return 'defaultLib:lib.d.ts'; + }; + TypeScriptLanguageServiceHost.prototype.isDefaultLibFileName = function (fileName) { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + }; + return TypeScriptLanguageServiceHost; +}()); +//#endregion +//#region Tree Shaking +var NodeColor; +(function (NodeColor) { + NodeColor[NodeColor["White"] = 0] = "White"; + NodeColor[NodeColor["Gray"] = 1] = "Gray"; + NodeColor[NodeColor["Black"] = 2] = "Black"; +})(NodeColor || (NodeColor = {})); +function getColor(node) { + return node.$$$color || 0 /* White */; +} +function setColor(node, color) { + node.$$$color = color; +} +function nodeOrParentIsBlack(node) { + while (node) { + var color = getColor(node); + if (color === 2 /* Black */) { + return true; + } + node = node.parent; + } + return false; +} +function nodeOrChildIsBlack(node) { + if (getColor(node) === 2 /* Black */) { + return true; + } + for (var _i = 0, _a = node.getChildren(); _i < _a.length; _i++) { + var child = _a[_i]; + if (nodeOrChildIsBlack(child)) { + return true; + } + } + return false; +} +function markNodes(languageService, options) { + var program = languageService.getProgram(); + if (options.shakeLevel === 0 /* Files */) { + // Mark all source files Black + program.getSourceFiles().forEach(function (sourceFile) { + setColor(sourceFile, 2 /* Black */); + }); + return; + } + var black_queue = []; + var gray_queue = []; + var sourceFilesLoaded = {}; + function enqueueTopLevelModuleStatements(sourceFile) { + sourceFile.forEachChild(function (node) { + if (ts.isImportDeclaration(node)) { + if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + if (ts.isExportDeclaration(node)) { + if (ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, 2 /* Black */); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + if (ts.isExpressionStatement(node) + || ts.isIfStatement(node) + || ts.isIterationStatement(node, true) + || ts.isExportAssignment(node)) { + enqueue_black(node); + } + if (ts.isImportEqualsDeclaration(node)) { + if (/export/.test(node.getFullText(sourceFile))) { + // e.g. "export import Severity = BaseSeverity;" + enqueue_black(node); + } + } + }); + } + function enqueue_gray(node) { + if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* Gray */) { + return; + } + setColor(node, 1 /* Gray */); + gray_queue.push(node); + } + function enqueue_black(node) { + var previousColor = getColor(node); + if (previousColor === 2 /* Black */) { + return; + } + if (previousColor === 1 /* Gray */) { + // remove from gray queue + gray_queue.splice(gray_queue.indexOf(node), 1); + setColor(node, 0 /* White */); + // add to black queue + enqueue_black(node); + // // move from one queue to the other + // black_queue.push(node); + // setColor(node, NodeColor.Black); + return; + } + if (nodeOrParentIsBlack(node)) { + return; + } + var fileName = node.getSourceFile().fileName; + if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { + setColor(node, 2 /* Black */); + return; + } + var sourceFile = node.getSourceFile(); + if (!sourceFilesLoaded[sourceFile.fileName]) { + sourceFilesLoaded[sourceFile.fileName] = true; + enqueueTopLevelModuleStatements(sourceFile); + } + if (ts.isSourceFile(node)) { + return; + } + setColor(node, 2 /* Black */); + black_queue.push(node); + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + var references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); + if (references) { + for (var i = 0, len = references.length; i < len; i++) { + var reference = references[i]; + var referenceSourceFile = program.getSourceFile(reference.fileName); + var referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + if (ts.isMethodDeclaration(referenceNode.parent) + || ts.isPropertyDeclaration(referenceNode.parent) + || ts.isGetAccessor(referenceNode.parent) + || ts.isSetAccessor(referenceNode.parent)) { + enqueue_gray(referenceNode.parent); + } + } + } + } + } + function enqueueFile(filename) { + var sourceFile = program.getSourceFile(filename); + if (!sourceFile) { + console.warn("Cannot find source file " + filename); + return; + } + enqueue_black(sourceFile); + } + function enqueueImport(node, importText) { + if (options.importIgnorePattern.test(importText)) { + // this import should be ignored + return; + } + var nodeSourceFile = node.getSourceFile(); + var fullPath; + if (/(^\.\/)|(^\.\.\/)/.test(importText)) { + fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + } + else { + fullPath = importText + '.ts'; + } + enqueueFile(fullPath); + } + options.entryPoints.forEach(function (moduleId) { return enqueueFile(moduleId + '.ts'); }); + // Add fake usage files + options.inlineEntryPoints.forEach(function (_, index) { return enqueueFile("inlineEntryPoint:" + index + ".ts"); }); + var step = 0; + var checker = program.getTypeChecker(); + var _loop_1 = function () { + ++step; + var node = void 0; + if (step % 100 === 0) { + console.log(step + "/" + (step + black_queue.length + gray_queue.length) + " (" + black_queue.length + ", " + gray_queue.length + ")"); + } + if (black_queue.length === 0) { + for (var i = 0; i < gray_queue.length; i++) { + var node_1 = gray_queue[i]; + var nodeParent = node_1.parent; + if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { + gray_queue.splice(i, 1); + black_queue.push(node_1); + setColor(node_1, 2 /* Black */); + i--; + } + } + } + if (black_queue.length > 0) { + node = black_queue.shift(); + } + else { + return "break"; + } + var nodeSourceFile = node.getSourceFile(); + var loop = function (node) { + var _a = getRealNodeSymbol(checker, node), symbol = _a[0], symbolImportNode = _a[1]; + if (symbolImportNode) { + setColor(symbolImportNode, 2 /* Black */); + } + if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + for (var i = 0, len = symbol.declarations.length; i < len; i++) { + var declaration = symbol.declarations[i]; + if (ts.isSourceFile(declaration)) { + // Do not enqueue full source files + // (they can be the declaration of a module import) + continue; + } + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + enqueue_black(declaration.name); + for (var j = 0; j < declaration.members.length; j++) { + var member = declaration.members[j]; + var memberName = member.name ? member.name.getText() : null; + if (ts.isConstructorDeclaration(member) + || ts.isConstructSignatureDeclaration(member) + || ts.isIndexSignatureDeclaration(member) + || ts.isCallSignatureDeclaration(member) + || memberName === 'toJSON' + || memberName === 'toString' + || memberName === 'dispose' // TODO: keeping all `dispose` methods + ) { + enqueue_black(member); + } + } + // queue the heritage clauses + if (declaration.heritageClauses) { + for (var _i = 0, _b = declaration.heritageClauses; _i < _b.length; _i++) { + var heritageClause = _b[_i]; + enqueue_black(heritageClause); + } + } + } + else { + enqueue_black(declaration); + } + } + } + node.forEachChild(loop); + }; + node.forEachChild(loop); + }; + while (black_queue.length > 0 || gray_queue.length > 0) { + var state_1 = _loop_1(); + if (state_1 === "break") + break; + } +} +function nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol) { + for (var i = 0, len = symbol.declarations.length; i < len; i++) { + var declaration = symbol.declarations[i]; + var declarationSourceFile = declaration.getSourceFile(); + if (nodeSourceFile === declarationSourceFile) { + if (declaration.pos <= node.pos && node.end <= declaration.end) { + return true; + } + } + } + return false; +} +function generateResult(languageService, shakeLevel) { + var program = languageService.getProgram(); + var result = {}; + var writeFile = function (filePath, contents) { + result[filePath] = contents; + }; + program.getSourceFiles().forEach(function (sourceFile) { + var fileName = sourceFile.fileName; + if (/^defaultLib:/.test(fileName)) { + return; + } + var destination = fileName; + if (/\.d\.ts$/.test(fileName)) { + if (nodeOrChildIsBlack(sourceFile)) { + writeFile(destination, sourceFile.text); + } + return; + } + var text = sourceFile.text; + var result = ''; + function keep(node) { + result += text.substring(node.pos, node.end); + } + function write(data) { + result += data; + } + function writeMarkedNodes(node) { + if (getColor(node) === 2 /* Black */) { + return keep(node); + } + // Always keep certain top-level statements + if (ts.isSourceFile(node.parent)) { + if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { + return keep(node); + } + if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { + return keep(node); + } + } + // Keep the entire import in import * as X cases + if (ts.isImportDeclaration(node)) { + if (node.importClause && node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + if (getColor(node.importClause.namedBindings) === 2 /* Black */) { + return keep(node); + } + } + else { + var survivingImports = []; + for (var i = 0; i < node.importClause.namedBindings.elements.length; i++) { + var importNode = node.importClause.namedBindings.elements[i]; + if (getColor(importNode) === 2 /* Black */) { + survivingImports.push(importNode.getFullText(sourceFile)); + } + } + var leadingTriviaWidth = node.getLeadingTriviaWidth(); + var leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingImports.length > 0) { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return write(leadingTrivia + "import " + node.importClause.name.text + ", {" + survivingImports.join(',') + " } from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + return write(leadingTrivia + "import {" + survivingImports.join(',') + " } from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + else { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return write(leadingTrivia + "import " + node.importClause.name.text + " from" + node.moduleSpecifier.getFullText(sourceFile) + ";"); + } + } + } + } + else { + if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + return keep(node); + } + } + } + if (shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + var toWrite = node.getFullText(); + for (var i = node.members.length - 1; i >= 0; i--) { + var member = node.members[i]; + if (getColor(member) === 2 /* Black */) { + // keep method + continue; + } + if (/^_(.*)Brand$/.test(member.name.getText())) { + // TODO: keep all members ending with `Brand`... + continue; + } + var pos = member.pos - node.pos; + var end = member.end - node.pos; + toWrite = toWrite.substring(0, pos) + toWrite.substring(end); + } + return write(toWrite); + } + if (ts.isFunctionDeclaration(node)) { + // Do not go inside functions if they haven't been marked + return; + } + node.forEachChild(writeMarkedNodes); + } + if (getColor(sourceFile) !== 2 /* Black */) { + if (!nodeOrChildIsBlack(sourceFile)) { + // none of the elements are reachable => don't write this file at all! + return; + } + sourceFile.forEachChild(writeMarkedNodes); + result += sourceFile.endOfFileToken.getFullText(sourceFile); + } + else { + result = text; + } + writeFile(destination, result); + }); + return result; +} +//#endregion +//#region Utils +/** + * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) + */ +function getRealNodeSymbol(checker, node) { + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + /* @internal */ + function getContainingObjectLiteralElement(node) { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + case ts.SyntaxKind.Identifier: + return ts.isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; + } + function getPropertySymbolsFromType(type, propName) { + function getTextOfPropertyName(name) { + function isStringOrNumericLiteral(node) { + var kind = node.kind; + return kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.NumericLiteral; + } + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return name.text; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + return name.text; + case ts.SyntaxKind.ComputedPropertyName: + return isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined; + } + } + var name = getTextOfPropertyName(propName); + if (name && type) { + var result = []; + var symbol_1 = type.getProperty(name); + if (type.flags & ts.TypeFlags.Union) { + for (var _i = 0, _a = type.types; _i < _a.length; _i++) { + var t = _a[_i]; + var symbol_2 = t.getProperty(name); + if (symbol_2) { + result.push(symbol_2); + } + } + return result; + } + if (symbol_1) { + result.push(symbol_1); + return result; + } + } + return undefined; + } + function getPropertySymbolsFromContextualType(typeChecker, node) { + var objectLiteral = node.parent; + var contextualType = typeChecker.getContextualType(objectLiteral); + return getPropertySymbolsFromType(contextualType, node.name); + } + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from an import. + // + function shouldSkipAlias(node, declaration) { + if (node.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (node.parent === declaration) { + return true; + } + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + case ts.SyntaxKind.ImportSpecifier: + return declaration.parent.kind === ts.SyntaxKind.NamedImports; + default: + return false; + } + } + if (!ts.isShorthandPropertyAssignment(node)) { + if (node.getChildCount() !== 0) { + return [null, null]; + } + } + var symbol = checker.getSymbolAtLocation(node); + var importNode = null; + if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + var aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + // We should mark the import as visited + importNode = symbol.declarations[0]; + symbol = aliased; + } + } + if (symbol) { + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + } + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(node.parent) && ts.isObjectBindingPattern(node.parent.parent) && + (node === (node.parent.propertyName || node.parent.name))) { + var type = checker.getTypeAtLocation(node.parent.parent); + if (type) { + var propSymbols = getPropertySymbolsFromType(type, node); + if (propSymbols) { + symbol = propSymbols[0]; + } + } + } + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: false }) + var element = getContainingObjectLiteralElement(node); + if (element && checker.getContextualType(element.parent)) { + var propertySymbols = getPropertySymbolsFromContextualType(checker, element); + if (propertySymbols) { + symbol = propertySymbols[0]; + } + } + } + if (symbol && symbol.declarations) { + return [symbol, importNode]; + } + return [null, null]; +} +/** Get the token whose text contains the position */ +function getTokenAtPosition(sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { + var current = sourceFile; + outer: while (true) { + // find the child that contains 'position' + for (var _i = 0, _a = current.getChildren(); _i < _a.length; _i++) { + var child = _a[_i]; + var start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + break; + } + var end = child.getEnd(); + if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; + } + } + return current; + } +} diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts new file mode 100644 index 00000000000..0527fe3ebce --- /dev/null +++ b/build/lib/treeshaking.ts @@ -0,0 +1,817 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); + +export const enum ShakeLevel { + Files = 0, + InnerFile = 1, + ClassMembers = 2 +} + +export interface ITreeShakingOptions { + /** + * The full path to the root where sources are. + */ + sourcesRoot: string; + /** + * Module ids. + * e.g. `vs/editor/editor.main` or `index` + */ + entryPoints: string[]; + /** + * Inline usages. + */ + inlineEntryPoints: string[]; + /** + * TypeScript libs. + * e.g. `lib.d.ts`, `lib.es2015.collection.d.ts` + */ + libs: string[]; + /** + * TypeScript compiler options. + */ + compilerOptions: ts.CompilerOptions; + /** + * The shake level to perform. + */ + shakeLevel: ShakeLevel; + /** + * regex pattern to ignore certain imports e.g. `vs/css!` imports + */ + importIgnorePattern: RegExp; + + redirects: { [module: string]: string; }; +} + +export interface ITreeShakingResult { + [file: string]: string; +} + +export function shake(options: ITreeShakingOptions): ITreeShakingResult { + const languageService = createTypeScriptLanguageService(options); + + markNodes(languageService, options); + + return generateResult(languageService, options.shakeLevel); +} + +//#region Discovery, LanguageService & Setup +function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.LanguageService { + // Discover referenced files + const FILES = discoverAndReadFiles(options); + + // Add fake usage files + options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { + FILES[`inlineEntryPoint:${index}.ts`] = inlineEntryPoint; + }); + + // Resolve libs + const RESOLVED_LIBS: ILibMap = {}; + options.libs.forEach((filename) => { + const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + RESOLVED_LIBS[`defaultLib:${filename}`] = fs.readFileSync(filepath).toString(); + }); + + const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, options.compilerOptions); + return ts.createLanguageService(host); +} + +/** + * Read imports and follow them until all files have been handled + */ +function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { + const FILES: IFileMap = {}; + + const in_queue: { [module: string]: boolean; } = Object.create(null); + const queue: string[] = []; + + const enqueue = (moduleId: string) => { + if (in_queue[moduleId]) { + return; + } + in_queue[moduleId] = true; + queue.push(moduleId); + }; + + options.entryPoints.forEach((entryPoint) => enqueue(entryPoint)); + + while (queue.length > 0) { + const moduleId = queue.shift(); + const dts_filename = path.join(options.sourcesRoot, moduleId + '.d.ts'); + if (fs.existsSync(dts_filename)) { + const dts_filecontents = fs.readFileSync(dts_filename).toString(); + FILES[moduleId + '.d.ts'] = dts_filecontents; + continue; + } + + let ts_filename: string; + if (options.redirects[moduleId]) { + ts_filename = path.join(options.sourcesRoot, options.redirects[moduleId] + '.ts'); + } else { + ts_filename = path.join(options.sourcesRoot, moduleId + '.ts'); + } + const ts_filecontents = fs.readFileSync(ts_filename).toString(); + const info = ts.preProcessFile(ts_filecontents); + for (let i = info.importedFiles.length - 1; i >= 0; i--) { + const importedFileName = info.importedFiles[i].fileName; + + if (options.importIgnorePattern.test(importedFileName)) { + // Ignore vs/css! imports + continue; + } + + let importedModuleId = importedFileName; + if (/(^\.\/)|(^\.\.\/)/.test(importedModuleId)) { + importedModuleId = path.join(path.dirname(moduleId), importedModuleId); + } + enqueue(importedModuleId); + } + + FILES[moduleId + '.ts'] = ts_filecontents; + } + + return FILES; +} + +interface ILibMap { [libName: string]: string; } +interface IFileMap { [fileName: string]: string; } + +/** + * A TypeScript language service host + */ +class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + + private readonly _libs: ILibMap; + private readonly _files: IFileMap; + private readonly _compilerOptions: ts.CompilerOptions; + + constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + + // --- language service host --------------- + + getCompilationSettings(): ts.CompilerOptions { + return this._compilerOptions; + } + getScriptFileNames(): string[] { + return ( + [] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files)) + ); + } + getScriptVersion(fileName: string): string { + return '1'; + } + getProjectVersion(): string { + return '1'; + } + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } else { + return ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(fileName: string): ts.ScriptKind { + return ts.ScriptKind.TS; + } + getCurrentDirectory(): string { + return ''; + } + getDefaultLibFileName(options: ts.CompilerOptions): string { + return 'defaultLib:lib.d.ts'; + } + isDefaultLibFileName(fileName: string): boolean { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} +//#endregion + +//#region Tree Shaking + +const enum NodeColor { + White = 0, + Gray = 1, + Black = 2 +} + +function getColor(node: ts.Node): NodeColor { + return (node).$$$color || NodeColor.White; +} +function setColor(node: ts.Node, color: NodeColor): void { + (node).$$$color = color; +} +function nodeOrParentIsBlack(node: ts.Node): boolean { + while (node) { + const color = getColor(node); + if (color === NodeColor.Black) { + return true; + } + node = node.parent; + } + return false; +} +function nodeOrChildIsBlack(node: ts.Node): boolean { + if (getColor(node) === NodeColor.Black) { + return true; + } + for (const child of node.getChildren()) { + if (nodeOrChildIsBlack(child)) { + return true; + } + } + return false; +} + +function markNodes(languageService: ts.LanguageService, options: ITreeShakingOptions) { + const program = languageService.getProgram(); + + if (options.shakeLevel === ShakeLevel.Files) { + // Mark all source files Black + program.getSourceFiles().forEach((sourceFile) => { + setColor(sourceFile, NodeColor.Black); + }); + return; + } + + const black_queue: ts.Node[] = []; + const gray_queue: ts.Node[] = []; + const sourceFilesLoaded: { [fileName: string]: boolean } = {}; + + function enqueueTopLevelModuleStatements(sourceFile: ts.SourceFile): void { + + sourceFile.forEachChild((node: ts.Node) => { + + if (ts.isImportDeclaration(node)) { + if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, NodeColor.Black); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + + if (ts.isExportDeclaration(node)) { + if (ts.isStringLiteral(node.moduleSpecifier)) { + setColor(node, NodeColor.Black); + enqueueImport(node, node.moduleSpecifier.text); + } + return; + } + + if ( + ts.isExpressionStatement(node) + || ts.isIfStatement(node) + || ts.isIterationStatement(node, true) + || ts.isExportAssignment(node) + ) { + enqueue_black(node); + } + + if (ts.isImportEqualsDeclaration(node)) { + if (/export/.test(node.getFullText(sourceFile))) { + // e.g. "export import Severity = BaseSeverity;" + enqueue_black(node); + } + } + + }); + } + + function enqueue_gray(node: ts.Node): void { + if (nodeOrParentIsBlack(node) || getColor(node) === NodeColor.Gray) { + return; + } + setColor(node, NodeColor.Gray); + gray_queue.push(node); + } + + function enqueue_black(node: ts.Node): void { + const previousColor = getColor(node); + + if (previousColor === NodeColor.Black) { + return; + } + + if (previousColor === NodeColor.Gray) { + // remove from gray queue + gray_queue.splice(gray_queue.indexOf(node), 1); + setColor(node, NodeColor.White); + + // add to black queue + enqueue_black(node); + + // // move from one queue to the other + // black_queue.push(node); + // setColor(node, NodeColor.Black); + return; + } + + if (nodeOrParentIsBlack(node)) { + return; + } + + const fileName = node.getSourceFile().fileName; + if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { + setColor(node, NodeColor.Black); + return; + } + + const sourceFile = node.getSourceFile(); + if (!sourceFilesLoaded[sourceFile.fileName]) { + sourceFilesLoaded[sourceFile.fileName] = true; + enqueueTopLevelModuleStatements(sourceFile); + } + + if (ts.isSourceFile(node)) { + return; + } + + setColor(node, NodeColor.Black); + black_queue.push(node); + + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); + if (references) { + for (let i = 0, len = references.length; i < len; i++) { + const reference = references[i]; + const referenceSourceFile = program.getSourceFile(reference.fileName); + const referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + if ( + ts.isMethodDeclaration(referenceNode.parent) + || ts.isPropertyDeclaration(referenceNode.parent) + || ts.isGetAccessor(referenceNode.parent) + || ts.isSetAccessor(referenceNode.parent) + ) { + enqueue_gray(referenceNode.parent); + } + } + } + } + } + + function enqueueFile(filename: string): void { + const sourceFile = program.getSourceFile(filename); + if (!sourceFile) { + console.warn(`Cannot find source file ${filename}`); + return; + } + enqueue_black(sourceFile); + } + + function enqueueImport(node: ts.Node, importText: string): void { + if (options.importIgnorePattern.test(importText)) { + // this import should be ignored + return; + } + + const nodeSourceFile = node.getSourceFile(); + let fullPath: string; + if (/(^\.\/)|(^\.\.\/)/.test(importText)) { + fullPath = path.join(path.dirname(nodeSourceFile.fileName), importText) + '.ts'; + } else { + fullPath = importText + '.ts'; + } + enqueueFile(fullPath); + } + + options.entryPoints.forEach(moduleId => enqueueFile(moduleId + '.ts')); + // Add fake usage files + options.inlineEntryPoints.forEach((_, index) => enqueueFile(`inlineEntryPoint:${index}.ts`)); + + let step = 0; + + const checker = program.getTypeChecker(); + while (black_queue.length > 0 || gray_queue.length > 0) { + ++step; + let node: ts.Node; + + if (step % 100 === 0) { + console.log(`${step}/${step+black_queue.length+gray_queue.length} (${black_queue.length}, ${gray_queue.length})`); + } + + if (black_queue.length === 0) { + for (let i = 0; i < gray_queue.length; i++) { + const node = gray_queue[i]; + const nodeParent = node.parent; + if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { + gray_queue.splice(i, 1); + black_queue.push(node); + setColor(node, NodeColor.Black); + i--; + } + } + } + + if (black_queue.length > 0) { + node = black_queue.shift(); + } else { + // only gray nodes remaining... + break; + } + const nodeSourceFile = node.getSourceFile(); + + const loop = (node: ts.Node) => { + const [symbol, symbolImportNode] = getRealNodeSymbol(checker, node); + if (symbolImportNode) { + setColor(symbolImportNode, NodeColor.Black); + } + + if (symbol && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + if (ts.isSourceFile(declaration)) { + // Do not enqueue full source files + // (they can be the declaration of a module import) + continue; + } + + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + enqueue_black(declaration.name); + + for (let j = 0; j < declaration.members.length; j++) { + const member = declaration.members[j]; + const memberName = member.name ? member.name.getText() : null; + if ( + ts.isConstructorDeclaration(member) + || ts.isConstructSignatureDeclaration(member) + || ts.isIndexSignatureDeclaration(member) + || ts.isCallSignatureDeclaration(member) + || memberName === 'toJSON' + || memberName === 'toString' + || memberName === 'dispose'// TODO: keeping all `dispose` methods + ) { + enqueue_black(member); + } + } + + // queue the heritage clauses + if (declaration.heritageClauses) { + for (let heritageClause of declaration.heritageClauses) { + enqueue_black(heritageClause); + } + } + } else { + enqueue_black(declaration); + } + } + } + node.forEachChild(loop); + }; + node.forEachChild(loop); + } +} + +function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, symbol: ts.Symbol): boolean { + for (let i = 0, len = symbol.declarations.length; i < len; i++) { + const declaration = symbol.declarations[i]; + const declarationSourceFile = declaration.getSourceFile(); + + if (nodeSourceFile === declarationSourceFile) { + if (declaration.pos <= node.pos && node.end <= declaration.end) { + return true; + } + } + } + + return false; +} + +function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { + const program = languageService.getProgram(); + + let result: ITreeShakingResult = {}; + const writeFile = (filePath: string, contents: string): void => { + result[filePath] = contents; + }; + + program.getSourceFiles().forEach((sourceFile) => { + const fileName = sourceFile.fileName; + if (/^defaultLib:/.test(fileName)) { + return; + } + const destination = fileName; + if (/\.d\.ts$/.test(fileName)) { + if (nodeOrChildIsBlack(sourceFile)) { + writeFile(destination, sourceFile.text); + } + return; + } + + let text = sourceFile.text; + let result = ''; + + function keep(node: ts.Node): void { + result += text.substring(node.pos, node.end); + } + function write(data: string): void { + result += data; + } + + function writeMarkedNodes(node: ts.Node): void { + if (getColor(node) === NodeColor.Black) { + return keep(node); + } + + // Always keep certain top-level statements + if (ts.isSourceFile(node.parent)) { + if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { + return keep(node); + } + + if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { + return keep(node); + } + } + + // Keep the entire import in import * as X cases + if (ts.isImportDeclaration(node)) { + if (node.importClause && node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + if (getColor(node.importClause.namedBindings) === NodeColor.Black) { + return keep(node); + } + } else { + let survivingImports: string[] = []; + for (let i = 0; i < node.importClause.namedBindings.elements.length; i++) { + const importNode = node.importClause.namedBindings.elements[i]; + if (getColor(importNode) === NodeColor.Black) { + survivingImports.push(importNode.getFullText(sourceFile)); + } + } + const leadingTriviaWidth = node.getLeadingTriviaWidth(); + const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); + if (survivingImports.length > 0) { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); + } else { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`); + } + } + } + } else { + if (node.importClause && getColor(node.importClause) === NodeColor.Black) { + return keep(node); + } + } + } + + if (shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + let toWrite = node.getFullText(); + for (let i = node.members.length - 1; i >= 0; i--) { + const member = node.members[i]; + if (getColor(member) === NodeColor.Black) { + // keep method + continue; + } + if (/^_(.*)Brand$/.test(member.name.getText())) { + // TODO: keep all members ending with `Brand`... + continue; + } + + let pos = member.pos - node.pos; + let end = member.end - node.pos; + toWrite = toWrite.substring(0, pos) + toWrite.substring(end); + } + return write(toWrite); + } + + if (ts.isFunctionDeclaration(node)) { + // Do not go inside functions if they haven't been marked + return; + } + + node.forEachChild(writeMarkedNodes); + } + + if (getColor(sourceFile) !== NodeColor.Black) { + if (!nodeOrChildIsBlack(sourceFile)) { + // none of the elements are reachable => don't write this file at all! + return; + } + sourceFile.forEachChild(writeMarkedNodes); + result += sourceFile.endOfFileToken.getFullText(sourceFile); + } else { + result = text; + } + + writeFile(destination, result); + }); + + return result; +} + +//#endregion + +//#region Utils + +/** + * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) + */ +function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol, ts.Declaration] { + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + /* @internal */ + function getContainingObjectLiteralElement(node: ts.Node): ts.ObjectLiteralElement | undefined { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + if (node.parent.kind === ts.SyntaxKind.ComputedPropertyName) { + return ts.isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + case ts.SyntaxKind.Identifier: + return ts.isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === ts.SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === ts.SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; + } + + function getPropertySymbolsFromType(type: ts.Type, propName: ts.PropertyName) { + function getTextOfPropertyName(name: ts.PropertyName): string { + + function isStringOrNumericLiteral(node: ts.Node): node is ts.StringLiteral | ts.NumericLiteral { + const kind = node.kind; + return kind === ts.SyntaxKind.StringLiteral + || kind === ts.SyntaxKind.NumericLiteral; + } + + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return name.text; + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + return name.text; + case ts.SyntaxKind.ComputedPropertyName: + return isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined!; + } + } + + const name = getTextOfPropertyName(propName); + if (name && type) { + const result: ts.Symbol[] = []; + const symbol = type.getProperty(name); + if (type.flags & ts.TypeFlags.Union) { + for (const t of (type).types) { + const symbol = t.getProperty(name); + if (symbol) { + result.push(symbol); + } + } + return result; + } + + if (symbol) { + result.push(symbol); + return result; + } + } + return undefined; + } + + function getPropertySymbolsFromContextualType(typeChecker: ts.TypeChecker, node: ts.ObjectLiteralElement): ts.Symbol[] { + const objectLiteral = node.parent; + const contextualType = typeChecker.getContextualType(objectLiteral)!; + return getPropertySymbolsFromType(contextualType, node.name!)!; + } + + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from an import. + // + function shouldSkipAlias(node: ts.Node, declaration: ts.Node): boolean { + if (node.kind !== ts.SyntaxKind.Identifier) { + return false; + } + if (node.parent === declaration) { + return true; + } + switch (declaration.kind) { + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.ImportEqualsDeclaration: + return true; + case ts.SyntaxKind.ImportSpecifier: + return declaration.parent.kind === ts.SyntaxKind.NamedImports; + default: + return false; + } + } + + if (!ts.isShorthandPropertyAssignment(node)) { + if (node.getChildCount() !== 0) { + return [null, null]; + } + } + + let symbol = checker.getSymbolAtLocation(node); + let importNode: ts.Declaration = null; + if (symbol && symbol.flags & ts.SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + // We should mark the import as visited + importNode = symbol.declarations[0]; + symbol = aliased; + } + } + + if (symbol) { + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { + symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + } + + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (ts.isPropertyName(node) && ts.isBindingElement(node.parent) && ts.isObjectBindingPattern(node.parent.parent) && + (node === (node.parent.propertyName || node.parent.name))) { + const type = checker.getTypeAtLocation(node.parent.parent); + if (type) { + const propSymbols = getPropertySymbolsFromType(type, node); + if (propSymbols) { + symbol = propSymbols[0]; + } + } + } + + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: false }) + const element = getContainingObjectLiteralElement(node); + if (element && checker.getContextualType(element.parent as ts.Expression)) { + const propertySymbols = getPropertySymbolsFromContextualType(checker, element); + if (propertySymbols) { + symbol = propertySymbols[0]; + } + } + } + + if (symbol && symbol.declarations) { + return [symbol, importNode]; + } + + return [null, null]; +} + +/** Get the token whose text contains the position */ +function getTokenAtPosition(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { + let current: ts.Node = sourceFile; + outer: while (true) { + // find the child that contains 'position' + for (const child of current.getChildren()) { + const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + break; + } + + const end = child.getEnd(); + if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; + } + } + + return current; + } +} + +//#endregion diff --git a/build/lib/tslint/duplicateImportsRule.js b/build/lib/tslint/duplicateImportsRule.js index 6acc9d728a6..1e06c4ab575 100644 --- a/build/lib/tslint/duplicateImportsRule.js +++ b/build/lib/tslint/duplicateImportsRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/tslint/importPatternsRule.js b/build/lib/tslint/importPatternsRule.js index 2b1c5cf4b9c..1f03b696996 100644 --- a/build/lib/tslint/importPatternsRule.js +++ b/build/lib/tslint/importPatternsRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/tslint/layeringRule.js b/build/lib/tslint/layeringRule.js index 2afb86e4238..7f35f6b5f5b 100644 --- a/build/lib/tslint/layeringRule.js +++ b/build/lib/tslint/layeringRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/tslint/noStandaloneEditorRule.js b/build/lib/tslint/noStandaloneEditorRule.js index 4bb57bbc322..29ed841de12 100644 --- a/build/lib/tslint/noStandaloneEditorRule.js +++ b/build/lib/tslint/noStandaloneEditorRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/tslint/noUnexternalizedStringsRule.js b/build/lib/tslint/noUnexternalizedStringsRule.js index d5ad583f442..f4ca615803a 100644 --- a/build/lib/tslint/noUnexternalizedStringsRule.js +++ b/build/lib/tslint/noUnexternalizedStringsRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/tslint/translationRemindRule.js b/build/lib/tslint/translationRemindRule.js index ec56aff1535..fe716a38ebe 100644 --- a/build/lib/tslint/translationRemindRule.js +++ b/build/lib/tslint/translationRemindRule.js @@ -4,9 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __extends = (this && this.__extends) || (function () { - var extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } diff --git a/build/lib/util.js b/build/lib/util.js index 1a2d40327cd..dd6f0b56992 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -14,7 +14,6 @@ var fs = require("fs"); var _rimraf = require("rimraf"); var git = require("./git"); var VinylFile = require("vinyl"); -var cp = require("child_process"); var NoCancellationToken = { isCancellationRequested: function () { return false; } }; function incremental(streamProvider, initial, supportsCancellation) { var input = es.through(); @@ -211,62 +210,6 @@ function filter(fn) { return result; } exports.filter = filter; -function tagExists(tagName) { - try { - cp.execSync("git rev-parse " + tagName, { stdio: 'ignore' }); - return true; - } - catch (e) { - return false; - } -} -/** - * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. - * Given 1.17.2, return 1.17.1 - * 1.18.0 => 1.17.2. (or the highest 1.17.x) - * 2.0.0 => 1.18.0 (or the highest 1.x) - */ -function getPreviousVersion(versionStr, _tagExists) { - if (_tagExists === void 0) { _tagExists = tagExists; } - function getLatestTagFromBase(semverArr, componentToTest) { - var baseVersion = semverArr.join('.'); - if (!_tagExists(baseVersion)) { - throw new Error('Failed to find git tag for base version, ' + baseVersion); - } - var goodTag; - do { - goodTag = semverArr.join('.'); - semverArr[componentToTest]++; - } while (_tagExists(semverArr.join('.'))); - return goodTag; - } - var semverArr = versionStringToNumberArray(versionStr); - if (semverArr[2] > 0) { - semverArr[2]--; - var previous = semverArr.join('.'); - if (!_tagExists(previous)) { - throw new Error('Failed to find git tag for previous version, ' + previous); - } - return previous; - } - else if (semverArr[1] > 0) { - semverArr[1]--; - return getLatestTagFromBase(semverArr, 2); - } - else { - semverArr[0]--; - // Find 1.x.0 for latest x - var latestMinorVersion = getLatestTagFromBase(semverArr, 1); - // Find 1.x.y for latest y - return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); - } -} -exports.getPreviousVersion = getPreviousVersion; -function versionStringToNumberArray(versionStr) { - return versionStr - .split('.') - .map(function (s) { return parseInt(s); }); -} function versionStringToNumber(versionStr) { var semverRegex = /(\d+)\.(\d+)\.(\d+)/; var match = versionStr.match(semverRegex); diff --git a/build/lib/util.ts b/build/lib/util.ts index 9dcbbe72484..e1393b86c5b 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -17,7 +17,6 @@ import * as git from './git'; import * as VinylFile from 'vinyl'; import { ThroughStream } from 'through'; import * as sm from 'source-map'; -import * as cp from 'child_process'; export interface ICancellationToken { isCancellationRequested(): boolean; @@ -271,66 +270,6 @@ export function filter(fn: (data: any) => boolean): FilterStream { return result; } -function tagExists(tagName: string): boolean { - try { - cp.execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -/** - * Returns the version previous to the given version. Throws if a git tag for that version doesn't exist. - * Given 1.17.2, return 1.17.1 - * 1.18.0 => 1.17.2. (or the highest 1.17.x) - * 2.0.0 => 1.18.0 (or the highest 1.x) - */ -export function getPreviousVersion(versionStr: string, _tagExists = tagExists) { - function getLatestTagFromBase(semverArr: number[], componentToTest: number): string { - const baseVersion = semverArr.join('.'); - if (!_tagExists(baseVersion)) { - throw new Error('Failed to find git tag for base version, ' + baseVersion); - } - - let goodTag; - do { - goodTag = semverArr.join('.'); - semverArr[componentToTest]++; - } while (_tagExists(semverArr.join('.'))); - - return goodTag; - } - - const semverArr = versionStringToNumberArray(versionStr); - if (semverArr[2] > 0) { - semverArr[2]--; - const previous = semverArr.join('.'); - if (!_tagExists(previous)) { - throw new Error('Failed to find git tag for previous version, ' + previous); - } - - return previous; - } else if (semverArr[1] > 0) { - semverArr[1]--; - return getLatestTagFromBase(semverArr, 2); - } else { - semverArr[0]--; - - // Find 1.x.0 for latest x - const latestMinorVersion = getLatestTagFromBase(semverArr, 1); - - // Find 1.x.y for latest y - return getLatestTagFromBase(versionStringToNumberArray(latestMinorVersion), 2); - } -} - -function versionStringToNumberArray(versionStr: string): number[] { - return versionStr - .split('.') - .map(s => parseInt(s)); -} - export function versionStringToNumber(versionStr: string) { const semverRegex = /(\d+)\.(\d+)\.(\d+)/; const match = versionStr.match(semverRegex); diff --git a/build/lib/watch/index.js b/build/lib/watch/index.js index e1138b07f90..94f73cdd4a0 100644 --- a/build/lib/watch/index.js +++ b/build/lib/watch/index.js @@ -19,16 +19,6 @@ function handleDeletions() { let watch = void 0; -// Disabled due to https://github.com/Microsoft/vscode/issues/36214 -// if (!process.env['VSCODE_USE_LEGACY_WATCH']) { -// try { -// watch = require('./watch-nsfw'); -// } catch (err) { -// console.warn('Could not load our cross platform file watcher: ' + err.toString()); -// console.warn('Falling back to our platform specific watcher...'); -// } -// } - if (!watch) { watch = process.platform === 'win32' ? require('./watch-win32') : require('gulp-watch'); } diff --git a/build/lib/watch/package.json b/build/lib/watch/package.json index b10e8ed2727..0d031340153 100644 --- a/build/lib/watch/package.json +++ b/build/lib/watch/package.json @@ -5,7 +5,6 @@ "author": "Microsoft ", "private": true, "devDependencies": { - "gulp-watch": "^4.3.9", - "nsfw": "^1.0.15" + "gulp-watch": "^4.3.9" } } diff --git a/build/lib/watch/watch-nsfw.js b/build/lib/watch/watch-nsfw.js deleted file mode 100644 index fb2b2758d02..00000000000 --- a/build/lib/watch/watch-nsfw.js +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -var nsfw = require('nsfw'); -var path = require('path'); -var fs = require('fs'); -var File = require('vinyl'); -var es = require('event-stream'); -var filter = require('gulp-filter'); - -function toChangeType(type) { - switch (type) { - case 0: return 'add'; - case 1: return 'unlink'; - case 2: return 'change'; - } -} - -function watch(root) { - var result = es.through(); - - function handleEvent(path, type) { - if (/[/\\].git[/\\]/.test(path) || /[/\\]out[/\\]/.test(path)) { - return; // filter as early as possible - } - - var file = new File({ - path: path, - base: root - }); - //@ts-ignore - file.event = type; - result.emit('data', file); - } - - nsfw(root, function (events) { - for (var i = 0; i < events.length; i++) { - var e = events[i]; - var changeType = e.action; - - if (changeType === 3 /* RENAMED */) { - handleEvent(path.join(e.directory, e.oldFile), 'unlink'); - handleEvent(path.join(e.directory, e.newFile), 'add'); - } else { - handleEvent(path.join(e.directory, e.file), toChangeType(changeType)); - } - } - }).then(function (watcher) { - watcher.start(); - }); - - return result; -} - -var cache = Object.create(null); - -module.exports = function (pattern, options) { - options = options || {}; - - var cwd = path.normalize(options.cwd || process.cwd()); - var watcher = cache[cwd]; - - if (!watcher) { - watcher = cache[cwd] = watch(cwd); - } - - var rebase = !options.base ? es.through() : es.mapSync(function (f) { - f.base = options.base; - return f; - }); - - return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) - .pipe(es.map(function (file, cb) { - fs.stat(file.path, function (err, stat) { - if (err && err.code === 'ENOENT') { return cb(null, file); } - if (err) { return cb(); } - if (!stat.isFile()) { return cb(); } - - fs.readFile(file.path, function (err, contents) { - if (err && err.code === 'ENOENT') { return cb(null, file); } - if (err) { return cb(); } - - file.contents = contents; - file.stat = stat; - cb(null, file); - }); - }); - })) - .pipe(rebase); -}; \ No newline at end of file diff --git a/build/lib/watch/yarn.lock b/build/lib/watch/yarn.lock index 0b4d3f70bb7..4fb4b56c0f6 100644 --- a/build/lib/watch/yarn.lock +++ b/build/lib/watch/yarn.lock @@ -61,10 +61,6 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -330,16 +326,6 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" -fs-extra@^0.26.5: - version "0.26.7" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -424,7 +410,7 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -649,12 +635,6 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - optionalDependencies: - graceful-fs "^4.1.6" - jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -680,12 +660,6 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" @@ -736,14 +710,6 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - -lodash.isundefined@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" - lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -835,7 +801,7 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" -nan@^2.0.0, nan@^2.3.0: +nan@^2.3.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" @@ -855,12 +821,6 @@ node-pre-gyp@^0.6.39: tar "^2.2.1" tar-pack "^3.4.0" -nodegit-promise@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/nodegit-promise/-/nodegit-promise-4.0.0.tgz#5722b184f2df7327161064a791d2e842c9167b34" - dependencies: - asap "~2.0.3" - nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -883,16 +843,6 @@ npmlog@^4.0.2: gauge "~2.7.3" set-blocking "~2.0.0" -nsfw@^1.0.15: - version "1.0.16" - resolved "https://registry.yarnpkg.com/nsfw/-/nsfw-1.0.16.tgz#78ba3e7f513b53d160c221b9018e0baf108614cc" - dependencies: - fs-extra "^0.26.5" - lodash.isinteger "^4.0.4" - lodash.isundefined "^3.0.1" - nan "^2.0.0" - promisify-node "^0.3.0" - number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -980,12 +930,6 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -promisify-node@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/promisify-node/-/promisify-node-0.3.0.tgz#b4b55acf90faa7d2b8b90ca396899086c03060cf" - dependencies: - nodegit-promise "~4.0.0" - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -1089,7 +1033,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: +rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: diff --git a/build/monaco/api.js b/build/monaco/api.js index 74e9273f35e..ae4a0d26616 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -134,7 +134,25 @@ function getTopLevelDeclaration(sourceFile, typeName) { function getNodeText(sourceFile, node) { return sourceFile.getFullText().substring(node.pos, node.end); } -function getMassagedTopLevelDeclarationText(sourceFile, declaration) { +function hasModifier(modifiers, kind) { + if (modifiers) { + for (var i = 0; i < modifiers.length; i++) { + var mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} +function isStatic(member) { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} +function isDefaultExport(declaration) { + return (hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)); +} +function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage) { var result = getNodeText(sourceFile, declaration); // if (result.indexOf('MonacoWorker') >= 0) { // console.log('here!'); @@ -142,6 +160,18 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration) { // } if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { var interfaceDeclaration = declaration; + var staticTypeName_1 = (isDefaultExport(interfaceDeclaration) + ? importName + ".default" + : importName + "." + declaration.name.text); + var instanceTypeName_1 = staticTypeName_1; + var typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + var arr = []; + for (var i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName_1 = instanceTypeName_1 + "<" + arr.join(',') + ">"; + } var members = interfaceDeclaration.members; members.forEach(function (member) { try { @@ -151,6 +181,15 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration) { result = result.replace(memberText, ''); // console.log('AFTER: ', result); } + else { + var memberName = member.name.text; + if (isStatic(member)) { + usage.push("a = " + staticTypeName_1 + "." + memberName + ";"); + } + else { + usage.push("a = (<" + instanceTypeName_1 + ">b)." + memberName + ";"); + } + } } catch (err) { // life.. @@ -211,6 +250,16 @@ function generateDeclarationFile(out, inputFiles, recipe) { var endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; var lines = recipe.split(endl); var result = []; + var usageCounter = 0; + var usageImports = []; + var usage = []; + usage.push("var a;"); + usage.push("var b;"); + var generateUsageImport = function (moduleId) { + var importName = 'm' + (++usageCounter); + usageImports.push("import * as " + importName + " from '" + moduleId.replace(/\.d\.ts$/, '') + "';"); + return importName; + }; lines.forEach(function (line) { var m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m1) { @@ -220,6 +269,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { if (!sourceFile_1) { return; } + var importName_1 = generateUsageImport(moduleId); var replacer_1 = createReplacer(m1[2]); var typeNames = m1[3].split(/,/); typeNames.forEach(function (typeName) { @@ -232,7 +282,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { logErr('Cannot find type ' + typeName); return; } - result.push(replacer_1(getMassagedTopLevelDeclarationText(sourceFile_1, declaration))); + result.push(replacer_1(getMassagedTopLevelDeclarationText(sourceFile_1, declaration, importName_1, usage))); }); return; } @@ -244,6 +294,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { if (!sourceFile_2) { return; } + var importName_2 = generateUsageImport(moduleId); var replacer_2 = createReplacer(m2[2]); var typeNames = m2[3].split(/,/); var typesToExcludeMap_1 = {}; @@ -271,7 +322,7 @@ function generateDeclarationFile(out, inputFiles, recipe) { } } } - result.push(replacer_2(getMassagedTopLevelDeclarationText(sourceFile_2, declaration))); + result.push(replacer_2(getMassagedTopLevelDeclarationText(sourceFile_2, declaration, importName_2, usage))); }); return; } @@ -282,9 +333,12 @@ function generateDeclarationFile(out, inputFiles, recipe) { resultTxt = resultTxt.replace(/\bEvent, kind: ts.SyntaxKind): boolean { + if (modifiers) { + for (let i = 0; i < modifiers.length; i++) { + let mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} -function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare): string { +function isStatic(member: ts.ClassElement | ts.TypeElement): boolean { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} + +function isDefaultExport(declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { + return ( + hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword) + ); +} + +function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[]): string { let result = getNodeText(sourceFile, declaration); // if (result.indexOf('MonacoWorker') >= 0) { // console.log('here!'); @@ -163,7 +185,23 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { let interfaceDeclaration = declaration; - let members: ts.NodeArray = interfaceDeclaration.members; + const staticTypeName = ( + isDefaultExport(interfaceDeclaration) + ? `${importName}.default` + : `${importName}.${declaration.name.text}` + ); + + let instanceTypeName = staticTypeName; + const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + let arr: string[] = []; + for (let i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`; + } + + const members: ts.NodeArray = interfaceDeclaration.members; members.forEach((member) => { try { let memberText = getNodeText(sourceFile, member); @@ -171,6 +209,13 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati // console.log('BEFORE: ', result); result = result.replace(memberText, ''); // console.log('AFTER: ', result); + } else { + const memberName = (member.name).text; + if (isStatic(member)) { + usage.push(`a = ${staticTypeName}.${memberName};`); + } else { + usage.push(`a = (<${instanceTypeName}>b).${memberName};`); + } } } catch (err) { // life.. @@ -237,11 +282,24 @@ function createReplacer(data: string): (str: string) => string { }; } -function generateDeclarationFile(out: string, inputFiles: { [file: string]: string; }, recipe: string): string { +function generateDeclarationFile(out: string, inputFiles: { [file: string]: string; }, recipe: string): [string, string] { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; let lines = recipe.split(endl); - let result = []; + let result: string[] = []; + + let usageCounter = 0; + let usageImports: string[] = []; + let usage: string[] = []; + + usage.push(`var a;`); + usage.push(`var b;`); + + const generateUsageImport = (moduleId: string) => { + let importName = 'm' + (++usageCounter); + usageImports.push(`import * as ${importName} from '${moduleId.replace(/\.d\.ts$/, '')}';`); + return importName; + }; lines.forEach(line => { @@ -254,6 +312,8 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri return; } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m1[2]); let typeNames = m1[3].split(/,/); @@ -267,7 +327,7 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri logErr('Cannot find type ' + typeName); return; } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration))); + result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage))); }); return; } @@ -281,6 +341,8 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri return; } + const importName = generateUsageImport(moduleId); + let replacer = createReplacer(m2[2]); let typeNames = m2[3].split(/,/); @@ -309,7 +371,7 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri } } } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration))); + result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage))); }); return; } @@ -324,10 +386,13 @@ function generateDeclarationFile(out: string, inputFiles: { [file: string]: stri resultTxt = format(resultTxt); - return resultTxt; + return [ + resultTxt, + `${usageImports.join('\n')}\n\n${usage.join('\n')}` + ]; } -export function getFilesToWatch(out: string): string[] { +function getIncludesInRecipe(): string[] { let recipe = fs.readFileSync(RECIPE_PATH).toString(); let lines = recipe.split(/\r\n|\n|\r/); let result = []; @@ -337,14 +402,14 @@ export function getFilesToWatch(out: string): string[] { let m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m1) { let moduleId = m1[1]; - result.push(moduleIdToPath(out, moduleId)); + result.push(moduleId); return; } let m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/); if (m2) { let moduleId = m2[1]; - result.push(moduleIdToPath(out, moduleId)); + result.push(moduleId); return; } }); @@ -352,8 +417,13 @@ export function getFilesToWatch(out: string): string[] { return result; } +export function getFilesToWatch(out: string): string[] { + return getIncludesInRecipe().map((moduleId) => moduleIdToPath(out, moduleId)); +} + export interface IMonacoDeclarationResult { content: string; + usageContent: string; filePath: string; isTheSame: boolean; } @@ -363,7 +433,7 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona SOURCE_FILE_MAP = {}; let recipe = fs.readFileSync(RECIPE_PATH).toString(); - let result = generateDeclarationFile(out, inputFiles, recipe); + let [result, usageContent] = generateDeclarationFile(out, inputFiles, recipe); let currentContent = fs.readFileSync(DECLARATION_PATH).toString(); log('Finished monaco.d.ts generation'); @@ -374,6 +444,7 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona return { content: result, + usageContent: usageContent, filePath: DECLARATION_PATH, isTheSame }; @@ -382,3 +453,96 @@ export function run(out: string, inputFiles: { [file: string]: string; }): IMona export function complainErrors() { logErr('Not running monaco.d.ts generation due to compile errors'); } + + + +interface ILibMap { [libName: string]: string; } +interface IFileMap { [fileName: string]: string; } + +class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + + private readonly _libs: ILibMap; + private readonly _files: IFileMap; + private readonly _compilerOptions: ts.CompilerOptions; + + constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + + // --- language service host --------------- + + getCompilationSettings(): ts.CompilerOptions { + return this._compilerOptions; + } + getScriptFileNames(): string[] { + return ( + [] + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files)) + ); + } + getScriptVersion(fileName: string): string { + return '1'; + } + getProjectVersion(): string { + return '1'; + } + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (this._files.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._files[fileName]); + } else if (this._libs.hasOwnProperty(fileName)) { + return ts.ScriptSnapshot.fromString(this._libs[fileName]); + } else { + return ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(fileName: string): ts.ScriptKind { + return ts.ScriptKind.TS; + } + getCurrentDirectory(): string { + return ''; + } + getDefaultLibFileName(options: ts.CompilerOptions): string { + return 'defaultLib:es5'; + } + isDefaultLibFileName(fileName: string): boolean { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} + +export function execute(): IMonacoDeclarationResult { + + const OUTPUT_FILES: { [file: string]: string; } = {}; + const SRC_FILES: IFileMap = {}; + const SRC_FILE_TO_EXPECTED_NAME: { [filename: string]: string; } = {}; + getIncludesInRecipe().forEach((moduleId) => { + if (/\.d\.ts$/.test(moduleId)) { + let fileName = path.join(SRC, moduleId); + OUTPUT_FILES[moduleIdToPath('src', moduleId)] = fs.readFileSync(fileName).toString(); + return; + } + + let fileName = path.join(SRC, moduleId) + '.ts'; + SRC_FILES[fileName] = fs.readFileSync(fileName).toString(); + SRC_FILE_TO_EXPECTED_NAME[fileName] = moduleIdToPath('src', moduleId); + }); + + const languageService = ts.createLanguageService(new TypeScriptLanguageServiceHost({}, SRC_FILES, {})); + + var t1 = Date.now(); + Object.keys(SRC_FILES).forEach((fileName) => { + var t = Date.now(); + const emitOutput = languageService.getEmitOutput(fileName, true); + OUTPUT_FILES[SRC_FILE_TO_EXPECTED_NAME[fileName]] = emitOutput.outputFiles[0].text; + // console.log(`Generating .d.ts for ${fileName} took ${Date.now() - t} ms`); + }); + console.log(`Generating .d.ts took ${Date.now() - t1} ms`); + + // console.log(result.filePath); + // fs.writeFileSync(result.filePath, result.content.replace(/\r\n/gm, '\n')); + // fs.writeFileSync(path.join(SRC, 'user.ts'), result.usageContent.replace(/\r\n/gm, '\n')); + + return run('src', OUTPUT_FILES); +} diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 218d4cdf8f5..d1b4e5d873a 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -25,13 +25,6 @@ declare namespace monaco { dispose(): void; } - export enum Severity { - Ignore = 0, - Info = 1, - Warning = 2, - Error = 3, - } - export enum MarkerTag { Unnecessary = 1, } @@ -43,8 +36,7 @@ declare namespace monaco { Error = 8, } - -#include(vs/base/common/winjs.base.d.ts): TValueCallback, ProgressCallback, Promise +#include(vs/base/common/winjs.base.d.ts): Promise #include(vs/base/common/cancellation): CancellationTokenSource, CancellationToken #include(vs/base/common/uri): URI, UriComponents #include(vs/editor/common/standalone/standaloneBase): KeyCode, KeyMod diff --git a/build/monaco/monaco.usage.recipe b/build/monaco/monaco.usage.recipe new file mode 100644 index 00000000000..beaad500aad --- /dev/null +++ b/build/monaco/monaco.usage.recipe @@ -0,0 +1,81 @@ + +// This file is adding references to various symbols which should not be removed via tree shaking + +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { SimpleWorkerClient, create as create1 } from 'vs/base/common/worker/simpleWorker'; +import { create as create2 } from 'vs/editor/common/services/editorSimpleWorker'; +import { QuickOpenWidget } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; +import { SyncDescriptor0, SyncDescriptor1, SyncDescriptor2, SyncDescriptor3, SyncDescriptor4, SyncDescriptor5, SyncDescriptor6, SyncDescriptor7, SyncDescriptor8 } from 'vs/platform/instantiation/common/descriptors'; +import { PolyfillPromise } from 'vs/base/common/winjs.polyfill.promise'; +import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; +import * as editorAPI from 'vs/editor/editor.api'; + +(function () { + var a: any; + var b: any; + a = (b).layout; // IContextViewProvider + a = (b).getWorkspaceFolder; // IWorkspaceFolderProvider + a = (b).getWorkspace; // IWorkspaceFolderProvider + a = (b).style; // IThemable + a = (b).style; // IThemable + a = (b).userHome; // IUserHomeProvider + a = (b).previous; // IDiffNavigator + a = (>b).type; + a = (b).start; + a = (b).end; + a = (>b).getProxyObject; // IWorkerClient + a = create1; + a = create2; + + // promise polyfill + a = PolyfillPromise.all; + a = PolyfillPromise.race; + a = PolyfillPromise.resolve; + a = PolyfillPromise.reject; + a = (b).then; + a = (b).catch; + + // injection madness + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + a = (>b).ctor; + a = (>b).bind; + + // exported API + a = editorAPI.CancellationTokenSource; + a = editorAPI.Emitter; + a = editorAPI.KeyCode; + a = editorAPI.KeyMod; + a = editorAPI.Position; + a = editorAPI.Range; + a = editorAPI.Selection; + a = editorAPI.SelectionDirection; + a = editorAPI.MarkerSeverity; + a = editorAPI.MarkerTag; + a = editorAPI.Promise; + a = editorAPI.Uri; + a = editorAPI.Token; + a = editorAPI.editor; + a = editorAPI.languages; +})(); diff --git a/build/monaco/package.json b/build/monaco/package.json index 256ca1ff534..efd919085b2 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,7 +1,7 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.12.0", + "version": "0.14.3", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 985fbd28cb7..eebf63ed330 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -43,7 +43,12 @@ function update(idOrPath) { let apiToken = process.env.TRANSIFEX_API_TOKEN; let languageId = localization.transifexId || localization.languageId; let translationDataFolder = path.join(locExtFolder, 'translations'); - + if (languageId === "zh-cn") { + languageId = "zh-hans"; + } + if (languageId === "zh-tw") { + languageId = "zh-hant"; + } if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { console.log('Clearing \'' + translationDataFolder + '\'...'); rimraf.sync(translationDataFolder); diff --git a/build/package.json b/build/package.json index 8f89abbc23d..9dd100c7361 100644 --- a/build/package.json +++ b/build/package.json @@ -14,7 +14,7 @@ "documentdb": "1.13.0", "mime": "^1.3.4", "minimist": "^1.2.0", - "typescript": "2.9.1", + "typescript": "3.0.3", "xml2js": "^0.4.17", "github-releases": "^0.4.1", "request": "^2.85.0" diff --git a/build/tfs/continuous-build.yml b/build/tfs/continuous-build.yml index 0bcd1e7cb2e..a1d4be71335 100644 --- a/build/tfs/continuous-build.yml +++ b/build/tfs/continuous-build.yml @@ -1,141 +1,17 @@ -phases: -- phase: Windows - queue: Hosted VS2017 +jobs: +- job: Windows + pool: + vmImage: VS2017-Win2016 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-task.Yarn@2 - displayName: Yarn - inputs: - customRegistry: useFeed - customFeed: 'd28dd4e6-8e53-406e-8887-22f7c4dffd0c' - # - task: npmAuthenticate@0 - # displayName: NPM Authenticate - - powershell: | - yarn gulp electron - displayName: Download Electron - - powershell: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - powershell: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - powershell: | - yarn compile - displayName: Compile Sources - - powershell: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - powershell: | - .\scripts\test.bat --tfs - displayName: Run Unit Tests - - powershell: | - .\scripts\test-integration.bat - displayName: Run Integration Tests - - powershell: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" - displayName: Run Smoke Tests - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-win32 - publishLocation: Container - continueOnError: true - condition: succeededOrFailed() + - template: win32/continuous-build-win32.yml -- phase: Linux - queue: Hosted Linux Preview +- job: Linux + pool: Hosted Linux Preview steps: - - script: | - set -e - apt-get update - apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 - cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb - chmod +x /etc/init.d/xvfb - update-rc.d xvfb defaults - ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon - service xvfb start - service dbus start - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-task.Yarn@2 - displayName: Yarn - inputs: - customRegistry: useFeed - customFeed: 'd28dd4e6-8e53-406e-8887-22f7c4dffd0c' - # - task: npmAuthenticate@0 - # displayName: NPM Authenticate - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - DISPLAY=:10 ./scripts/test.sh --tfs - displayName: Run Unit Tests + - template: linux/continuous-build-linux.yml -- phase: macOS - queue: Hosted macOS Preview +- job: macOS + pool: + vmImage: macOS 10.13 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-task.Yarn@2 - displayName: Yarn - inputs: - customRegistry: useFeed - customFeed: 'd28dd4e6-8e53-406e-8887-22f7c4dffd0c' - # - task: npmAuthenticate@0 - # displayName: NPM Authenticate - - script: | - yarn gulp electron-x64 - displayName: Download Electron - - script: | - yarn gulp hygiene - displayName: Run Hygiene Checks - - script: | - yarn check-monaco-editor-compilation - displayName: Run Monaco Editor Checks - - script: | - yarn compile - displayName: Compile Sources - - script: | - yarn download-builtin-extensions - displayName: Download Built-in Extensions - - script: | - ./scripts/test.sh --tfs - displayName: Run Unit Tests - - script: | - ./scripts/test-integration.sh - displayName: Run Integration Tests - - script: | - yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" - displayName: Run Smoke Tests - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' - ArtifactName: build-artifacts-darwin - publishLocation: Container - continueOnError: true - condition: succeededOrFailed() \ No newline at end of file + - template: darwin/continuous-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/darwin/continuous-build-darwin.yml b/build/tfs/darwin/continuous-build-darwin.yml new file mode 100644 index 00000000000..9d3f6254cd6 --- /dev/null +++ b/build/tfs/darwin/continuous-build-darwin.yml @@ -0,0 +1,48 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn monaco-compile-check + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- script: | + ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: Run Integration Tests +- script: | + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)/artifacts" --log "$(Build.ArtifactStagingDirectory)/artifacts/smoketest.log" + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-darwin + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/darwin/enqueue.js b/build/tfs/darwin/enqueue.js index 3b870657134..2de6022e1fe 100644 --- a/build/tfs/darwin/enqueue.js +++ b/build/tfs/darwin/enqueue.js @@ -18,8 +18,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) { function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { - if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [0, t.value]; + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; diff --git a/build/tfs/darwin/product-build-darwin.yml b/build/tfs/darwin/product-build-darwin.yml new file mode 100644 index 00000000000..b783a2b5270 --- /dev/null +++ b/build/tfs/darwin/product-build-darwin.yml @@ -0,0 +1,86 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" + +- script: | + set -e + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + yarn gulp -- hygiene + yarn monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" yarn gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + displayName: Prepare build + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + yarn gulp -- vscode-darwin-min upload-vscode-sourcemaps + displayName: Build + +- script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + # APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" + displayName: Run unit tests + +- script: | + set -e + pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin.zip * && popd + displayName: Archive build + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)' + Pattern: 'VSCode-darwin.zip' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-401337-Apple", + "operationSetCode": "MacAppDeveloperSign", + "parameters": [ ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + displayName: Codesign + +- script: | + set -e + + # remove pkg from archive + zip -d ../VSCode-darwin.zip "*.pkg" + + # publish the build + PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` + VERSION=`node -p "require(\"$PACKAGEJSON\").version"` + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js \ + "$(VSCODE_QUALITY)" \ + darwin \ + archive \ + "VSCode-darwin-$(VSCODE_QUALITY).zip" \ + $VERSION \ + true \ + ../VSCode-darwin.zip + + # publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" + + # upload configuration + AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ + yarn gulp -- upload-vscode-configuration + displayName: Publish \ No newline at end of file diff --git a/build/tfs/linux/continuous-build-linux.yml b/build/tfs/linux/continuous-build-linux.yml new file mode 100644 index 00000000000..9c2e125447b --- /dev/null +++ b/build/tfs/linux/continuous-build-linux.yml @@ -0,0 +1,44 @@ +steps: +- script: | + set -e + apt-get update + apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 + cp build/tfs/linux/x64/xvfb.init /etc/init.d/xvfb + chmod +x /etc/init.d/xvfb + update-rc.d xvfb defaults + ln -sf /bin/dbus-daemon /usr/bin/dbus-daemon + service xvfb start + service dbus start +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" +- script: | + yarn + displayName: Install Dependencies +- script: | + yarn gulp electron-x64 + displayName: Download Electron +- script: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- script: | + yarn monaco-compile-check + displayName: Run Monaco Editor Checks +- script: | + yarn compile + displayName: Compile Sources +- script: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- script: | + DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" + displayName: Run Unit Tests +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() \ No newline at end of file diff --git a/build/tfs/linux/frozen-check.ts b/build/tfs/linux/frozen-check.ts index 489a24dc28a..c97d33b3953 100644 --- a/build/tfs/linux/frozen-check.ts +++ b/build/tfs/linux/frozen-check.ts @@ -39,4 +39,11 @@ function getConfig(quality: string): Promise { } getConfig(process.argv[2]) - .then(c => console.log(c.frozen), e => console.error(e)); \ No newline at end of file + .then(config => { + console.log(config.frozen); + process.exit(0); + }) + .catch(err => { + console.error(err); + process.exit(1); + }); \ No newline at end of file diff --git a/build/tfs/linux/product-build-linux.yml b/build/tfs/linux/product-build-linux.yml new file mode 100644 index 00000000000..3e1d2e8c9a7 --- /dev/null +++ b/build/tfs/linux/product-build-linux.yml @@ -0,0 +1,95 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" + +- script: | + set -e + export npm_config_arch="$(VSCODE_ARCH)" + if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then + export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" + fi + + echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc + yarn + npm run gulp -- hygiene + npm run monaco-compile-check + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin + node build/tfs/common/installDistro.js + node build/lib/builtInExtensions.js + +- script: | + set -e + VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min + name: build + +- script: | + set -e + npm run gulp -- "electron-$(VSCODE_ARCH)" + + # xvfb seems to be crashing often, let's make sure it's always up + service xvfb start + + DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" + # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" + name: test + +- script: | + set -e + REPO="$(pwd)" + ROOT="$REPO/.." + ARCH="$(VSCODE_ARCH)" + + # Publish tarball + PLATFORM_LINUX="linux-$(VSCODE_ARCH)" + [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" + [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" + BUILDNAME="VSCode-$PLATFORM_LINUX" + BUILD="$ROOT/$BUILDNAME" + BUILD_VERSION="$(date +%s)" + [ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" + TARBALL_PATH="$ROOT/$TARBALL_FILENAME" + PACKAGEJSON="$BUILD/resources/app/package.json" + VERSION=$(node -p "require(\"$PACKAGEJSON\").version") + + rm -rf $ROOT/code-*.tar.* + (cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$VERSION" true "$TARBALL_PATH" + + # Publish hockeyapp symbols + node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" + + # Publish DEB + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" + PLATFORM_DEB="linux-deb-$ARCH" + [[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" + DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" + DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_DEB" package "$DEB_FILENAME" "$VERSION" true "$DEB_PATH" + + # Publish RPM + npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" + PLATFORM_RPM="linux-rpm-$ARCH" + [[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" + RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" + RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/tfs/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_RPM" package "$RPM_FILENAME" "$VERSION" true "$RPM_PATH" + + # SNAP_FILENAME="$(ls $REPO/.build/linux/snap/$ARCH/ | grep .snap)" + # SNAP_PATH="$REPO/.build/linux/snap/$ARCH/$SNAP_FILENAME" diff --git a/build/tfs/linux/release.sh b/build/tfs/linux/release.sh deleted file mode 100755 index ad74ce0a385..00000000000 --- a/build/tfs/linux/release.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -set -e - -# Arguments -ARCH="$1" -LINUX_REPO_PASSWORD="$2" - -# Variables -PLATFORM_LINUX="linux-$ARCH" -PLATFORM_DEB="linux-deb-$ARCH" -PLATFORM_RPM="linux-rpm-$ARCH" -[[ "$ARCH" == "ia32" ]] && DEB_ARCH="i386" || DEB_ARCH="amd64" -[[ "$ARCH" == "ia32" ]] && RPM_ARCH="i386" || RPM_ARCH="x86_64" -REPO="`pwd`" -ROOT="$REPO/.." -BUILDNAME="VSCode-$PLATFORM_LINUX" -BUILD="$ROOT/$BUILDNAME" -BUILD_VERSION="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/ | sed -e 's/code-[a-z]*_//g' -e 's/\.deb$//g')" -[ -z "$VSCODE_QUALITY" ] && TARBALL_FILENAME="code-$BUILD_VERSION.tar.gz" || TARBALL_FILENAME="code-$VSCODE_QUALITY-$BUILD_VERSION.tar.gz" -TARBALL_PATH="$ROOT/$TARBALL_FILENAME" -PACKAGEJSON="$BUILD/resources/app/package.json" -VERSION=$(node -p "require(\"$PACKAGEJSON\").version") - -rm -rf $ROOT/code-*.tar.* -(cd $ROOT && tar -czf $TARBALL_PATH $BUILDNAME) - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_LINUX archive-unsigned $TARBALL_FILENAME $VERSION true $TARBALL_PATH - -DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" -DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_DEB package $DEB_FILENAME $VERSION true $DEB_PATH - -RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" -RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" - -node build/tfs/common/publish.js $VSCODE_QUALITY $PLATFORM_RPM package $RPM_FILENAME $VERSION true $RPM_PATH - -# SNAP_FILENAME="$(ls $REPO/.build/linux/snap/$ARCH/ | grep .snap)" -# SNAP_PATH="$REPO/.build/linux/snap/$ARCH/$SNAP_FILENAME" - -IS_FROZEN="$(node build/tfs/linux/frozen-check.js $VSCODE_QUALITY)" - -if [ -z "$VSCODE_QUALITY" ]; then - echo "VSCODE_QUALITY is not set, skipping repo package publish" -elif [ "$IS_FROZEN" = "true" ]; then - echo "$VSCODE_QUALITY is frozen, skipping repo package publish" -else - if [ "$BUILD_SOURCEBRANCH" = "master" ] || [ "$BUILD_SOURCEBRANCH" = "refs/heads/master" ]; then - if [[ $BUILD_QUEUEDBY = *"Project Collection Service Accounts"* || $BUILD_QUEUEDBY = *"Microsoft.VisualStudio.Services.TFS"* ]]; then - # Write config files needed by API, use eval to force environment variable expansion - pushd build/tfs/linux - # Submit to apt repo - if [ "$DEB_ARCH" = "amd64" ]; then - eval echo '{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4adf642421134a1a48d1a\", \"username\": \"vscode\", \"password\": \"$LINUX_REPO_PASSWORD\" }' > apt-config.json - - ./repoapi_client.sh -config apt-config.json -addfile $DEB_PATH - fi - # Submit to yum repo (disabled as it's manual until signing is automated) - # eval echo '{ \"server\": \"azure-apt-cat.cloudapp.net\", \"protocol\": \"https\", \"port\": \"443\", \"repositoryId\": \"58a4ae3542421134a1a48d1b\", \"username\": \"vscode\", \"password\": \"$LINUX_REPO_PASSWORD\" }' > yum-config.json - - # ./repoapi_client.sh -config yum-config.json -addfile $RPM_PATH - popd - echo "To check repo publish status run ./repoapi_client.sh -config config.json -check " - fi - fi -fi diff --git a/build/tfs/product-build.yml b/build/tfs/product-build.yml index 35f64ea2dad..b437720a43a 100644 --- a/build/tfs/product-build.yml +++ b/build/tfs/product-build.yml @@ -1,413 +1,41 @@ -phases: -- phase: Windows +jobs: +- job: Windows condition: eq(variables['VSCODE_BUILD_WIN32'], 'true') - queue: - name: Hosted VS2017 - parallel: 2 - matrix: - x64: - VSCODE_ARCH: x64 - ia32: - VSCODE_ARCH: ia32 - - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - powershell: | - $ErrorActionPreference = "Stop" - "machine monacotools.visualstudio.com password $(VSO_PAT)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII - $env:npm_config_arch="$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - powershell: | - $ErrorActionPreference = "Stop" - $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-copy-inno-updater" - name: build - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "electron-$(VSCODE_ARCH)" - .\scripts\test.bat --build --tfs - # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - name: test - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 - inputs: - ConnectedServiceName: 'ESRP CodeSign' - FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' - Pattern: '*.dll,*.exe,*.node' - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "keyCode": "CP-229803", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "Append", - "parameterValue": "/as" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - } - ] - SessionTimeout: 120 - - - powershell: | - $ErrorActionPreference = "Stop" - npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-setup" - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 - inputs: - ConnectedServiceName: 'ESRP CodeSign' - FolderPath: '$(agent.builddirectory)' - Pattern: VSCodeSetup.exe - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "keyCode": "CP-229803", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "VS Code" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://code.visualstudio.com/" - }, - { - "parameterName": "Append", - "parameterValue": "/as" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - } - ] - SessionTimeout: 120 - - - powershell: | - $ErrorActionPreference = "Stop" - $Repo = "$(pwd)" - $Root = "$Repo\.." - $Exe = "$Repo\.build\win32-$(VSCODE_ARCH)\setup\VSCodeSetup.exe" - $Zip = "$Repo\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip" - $Build = "$Root\VSCode-win32-$(VSCODE_ARCH)" - - # get version - $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json - $Version = $PackageJson.version - $Quality = "$env:VSCODE_QUALITY" - $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" - $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" - $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" - - $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } - - node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip - node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $Exe - - # publish hockeyapp symbols - $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId - -- phase: Linux - condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') - queue: linux-x64 + pool: + vmImage: VS2017-Win2016 variables: VSCODE_ARCH: x64 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" + - template: win32/product-build-win32.yml - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi - - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release2.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX64)" - -- phase: Linux32 - condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') - queue: linux-ia32 +- job: Windows32 + condition: eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true') + pool: + vmImage: VS2017-Win2016 variables: VSCODE_ARCH: ia32 - steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" + - template: win32/product-build-win32.yml - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" +- job: Linux + condition: eq(variables['VSCODE_BUILD_LINUX'], 'true') + pool: linux-x64 + variables: + VSCODE_ARCH: x64 + steps: + - template: linux/product-build-linux.yml - - script: | - set -e - export npm_config_arch="$(VSCODE_ARCH)" - if [[ "$(VSCODE_ARCH)" == "ia32" ]]; then - export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" - fi +- job: Linux32 + condition: eq(variables['VSCODE_BUILD_LINUX_32BIT'], 'true') + pool: linux-ia32 + variables: + VSCODE_ARCH: ia32 + steps: + - template: linux/product-build-linux.yml - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- vscode-linux-$(VSCODE_ARCH)-min - name: build - - - script: | - set -e - npm run gulp -- "electron-$(VSCODE_ARCH)" - DISPLAY=:10 ./scripts/test.sh --build --tfs - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" - name: test - - - script: | - set -e - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-deb" - npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-rpm" - #npm run gulp -- "vscode-linux-$(VSCODE_ARCH)-build-snap" - - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - ./build/tfs/linux/release.sh "$(VSCODE_ARCH)" "$(LINUX_REPO_PASSWORD)" - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_LINUX32)" - -- phase: macOS +- job: macOS condition: eq(variables['VSCODE_BUILD_MACOS'], 'true') - queue: Hosted macOS Preview + pool: + vmImage: macOS 10.13 steps: - - task: NodeTool@0 - inputs: - versionSpec: "8.9.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.3.2" - - - script: | - set -e - echo "machine monacotools.visualstudio.com password $(VSO_PAT)" > ~/.netrc - yarn - npm run gulp -- hygiene - npm run monaco-compile-check - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" npm run gulp -- mixin - node build/tfs/common/installDistro.js - node build/lib/builtInExtensions.js - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" \ - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- vscode-darwin-min upload-vscode-sourcemaps - name: build - - - script: | - set -e - ./scripts/test.sh --build --tfs - APP_NAME="`ls $(agent.builddirectory)/VSCode-darwin | head -n 1`" - # yarn smoketest -- --build "$(agent.builddirectory)/VSCode-darwin/$APP_NAME" - name: test - - - script: | - set -e - # archive the unsigned build - pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin-unsigned.zip * && popd - - # publish the unsigned build - PACKAGEJSON=`ls ../VSCode-darwin/*.app/Contents/Resources/app/package.json` - VERSION=`node -p "require(\"$PACKAGEJSON\").version"` - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ - node build/tfs/common/publish.js \ - "$(VSCODE_QUALITY)" \ - darwin \ - archive-unsigned \ - "VSCode-darwin-$(VSCODE_QUALITY)-unsigned.zip" \ - $VERSION \ - false \ - ../VSCode-darwin-unsigned.zip - - # publish hockeyapp symbols - node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" "$(VSCODE_HOCKEYAPP_ID_MACOS)" - - # enqueue the unsigned build - AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ - AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - node build/tfs/darwin/enqueue.js "$(VSCODE_QUALITY)" - - AZURE_STORAGE_ACCESS_KEY="$(AZURE_STORAGE_ACCESS_KEY)" \ - npm run gulp -- upload-vscode-configuration \ No newline at end of file + - template: darwin/product-build-darwin.yml \ No newline at end of file diff --git a/build/tfs/win32/ESRPClient/NuGet.config b/build/tfs/win32/ESRPClient/NuGet.config new file mode 100644 index 00000000000..6d6da347fd2 --- /dev/null +++ b/build/tfs/win32/ESRPClient/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build/tfs/win32/ESRPClient/packages.config b/build/tfs/win32/ESRPClient/packages.config new file mode 100644 index 00000000000..d7a6f144f47 --- /dev/null +++ b/build/tfs/win32/ESRPClient/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/build/tfs/win32/continuous-build-win32.yml b/build/tfs/win32/continuous-build-win32.yml new file mode 100644 index 00000000000..780c9a0e197 --- /dev/null +++ b/build/tfs/win32/continuous-build-win32.yml @@ -0,0 +1,48 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" +- powershell: | + yarn + displayName: Install Dependencies +- powershell: | + yarn gulp electron + displayName: Download Electron +- powershell: | + yarn gulp hygiene + displayName: Run Hygiene Checks +- powershell: | + yarn monaco-compile-check + displayName: Run Monaco Editor Checks +- powershell: | + yarn compile + displayName: Compile Sources +- powershell: | + yarn download-builtin-extensions + displayName: Download Built-in Extensions +- powershell: | + .\scripts\test.bat --tfs "Unit Tests" + displayName: Run Unit Tests +- powershell: | + .\scripts\test-integration.bat --tfs "Integration Tests" + displayName: Run Integration Tests +- powershell: | + yarn smoketest --screenshots "$(Build.ArtifactStagingDirectory)\artifacts" --log "$(Build.ArtifactStagingDirectory)\artifacts\smoketest.log" + displayName: Run Smoke Tests + continueOnError: true +- task: PublishBuildArtifacts@1 + displayName: Publish Smoketest Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + ArtifactName: build-artifacts-win32 + publishLocation: Container + condition: eq(variables['System.PullRequest.IsFork'], 'False') +- task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: '*-results.xml' + searchFolder: '$(Build.ArtifactStagingDirectory)/test-results' + condition: succeededOrFailed() diff --git a/build/tfs/win32/exec.ps1 b/build/tfs/win32/exec.ps1 new file mode 100644 index 00000000000..826cefdf7dd --- /dev/null +++ b/build/tfs/win32/exec.ps1 @@ -0,0 +1,24 @@ +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. + +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} \ No newline at end of file diff --git a/build/tfs/win32/import-esrp-auth-cert.ps1 b/build/tfs/win32/import-esrp-auth-cert.ps1 new file mode 100644 index 00000000000..c345c780231 --- /dev/null +++ b/build/tfs/win32/import-esrp-auth-cert.ps1 @@ -0,0 +1,14 @@ +Param( + [string]$AuthCertificateBase64, + [string]$AuthCertificateKey +) + +# Import auth certificate +$AuthCertificateFileName = [System.IO.Path]::GetTempFileName() +$AuthCertificateBytes = [Convert]::FromBase64String($AuthCertificateBase64) +[IO.File]::WriteAllBytes($AuthCertificateFileName, $AuthCertificateBytes) +$AuthCertificate = Import-PfxCertificate -FilePath $AuthCertificateFileName -CertStoreLocation Cert:\LocalMachine\My -Password (ConvertTo-SecureString $AuthCertificateKey -AsPlainText -Force) +rm $AuthCertificateFileName +$ESRPAuthCertificateSubjectName = $AuthCertificate.Subject + +Write-Output ("##vso[task.setvariable variable=ESRPAuthCertificateSubjectName;]$ESRPAuthCertificateSubjectName") \ No newline at end of file diff --git a/build/tfs/win32/product-build-win32.yml b/build/tfs/win32/product-build-win32.yml new file mode 100644 index 00000000000..31288bdbdda --- /dev/null +++ b/build/tfs/win32/product-build-win32.yml @@ -0,0 +1,166 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "8.9.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + "machine monacotools.visualstudio.com password $(VSO_PAT)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII + $env:npm_config_arch="$(VSCODE_ARCH)" + $env:CHILD_CONCURRENCY="1" + $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" + exec { yarn } + exec { npm run gulp -- hygiene } + exec { npm run monaco-compile-check } + exec { npm run gulp -- mixin } + exec { node build/tfs/common/installDistro.js } + exec { node build/lib/builtInExtensions.js } + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(VSCODE_MIXIN_PASSWORD)" + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-min" } + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-copy-inno-updater" } + name: build + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp -- "electron-$(VSCODE_ARCH)" } + exec { .\scripts\test.bat --build --tfs "Unit Tests" } + # yarn smoketest -- --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + name: test + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' + Pattern: '*.dll,*.exe,*.node' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-229803", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/t \"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS\"" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "Append", + "parameterValue": "/as" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + +- task: NuGetCommand@2 + displayName: Install ESRPClient.exe + inputs: + restoreSolution: 'build\tfs\win32\ESRPClient\packages.config' + feedsToUse: config + nugetConfigPath: 'build\tfs\win32\ESRPClient\NuGet.config' + externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b + restoreDirectory: packages + +- task: ESRPImportCertTask@1 + displayName: Import ESRP Request Signing Certificate + inputs: + ESRP: 'ESRP CodeSign' + +- powershell: | + $ErrorActionPreference = "Stop" + .\build\tfs\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(ESRP_AUTH_CERTIFICATE) -AuthCertificateKey $(ESRP_AUTH_CERTIFICATE_KEY) + displayName: Import ESRP Auth Certificate + +- powershell: | + . build/tfs/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp -- "vscode-win32-$(VSCODE_ARCH)-archive" "vscode-win32-$(VSCODE_ARCH)-system-setup" "vscode-win32-$(VSCODE_ARCH)-user-setup" } + + $Repo = "$(pwd)" + $Root = "$Repo\.." + $SystemExe = "$Repo\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe" + $UserExe = "$Repo\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe" + $Zip = "$Repo\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip" + $Build = "$Root\VSCode-win32-$(VSCODE_ARCH)" + + # get version + $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json + $Version = $PackageJson.version + $Quality = "$env:VSCODE_QUALITY" + $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" + $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" + + $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } + + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform-archive" archive "VSCode-win32-$(VSCODE_ARCH)-$Version.zip" $Version true $Zip } + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform" setup "VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $SystemExe } + exec { node build/tfs/common/publish.js $Quality "$global:assetPlatform-user" setup "VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" $Version true $UserExe } + + # publish hockeyapp symbols + $hockeyAppId = if ("$(VSCODE_ARCH)" -eq "ia32") { "$(VSCODE_HOCKEYAPP_ID_WIN32)" } else { "$(VSCODE_HOCKEYAPP_ID_WIN64)" } + exec { node build/tfs/common/symbols.js "$(VSCODE_MIXIN_PASSWORD)" "$(VSCODE_HOCKEYAPP_TOKEN)" "$(VSCODE_ARCH)" $hockeyAppId } diff --git a/build/tfs/win32/sign.ps1 b/build/tfs/win32/sign.ps1 new file mode 100644 index 00000000000..d888a7d104f --- /dev/null +++ b/build/tfs/win32/sign.ps1 @@ -0,0 +1,82 @@ +function Create-TmpJson($Obj) { + $FileName = [System.IO.Path]::GetTempFileName() + ConvertTo-Json -Depth 100 $Obj | Out-File -Encoding UTF8 $FileName + return $FileName +} + +$Auth = Create-TmpJson @{ + Version = "1.0.0" + AuthenticationType = "AAD_CERT" + ClientId = $env:ESRPClientId + AuthCert = @{ + SubjectName = $env:ESRPAuthCertificateSubjectName + StoreLocation = "LocalMachine" + StoreName = "My" + } + RequestSigningCert = @{ + SubjectName = $env:ESRPCertificateSubjectName + StoreLocation = "LocalMachine" + StoreName = "My" + } +} + +$Policy = Create-TmpJson @{ + Version = "1.0.0" +} + +$Input = Create-TmpJson @{ + Version = "1.0.0" + SignBatches = @( + @{ + SourceLocationType = "UNC" + SignRequestFiles = @( + @{ + SourceLocation = $args[0] + } + ) + SigningInfo = @{ + Operations = @( + @{ + KeyCode = "CP-229803" + OperationCode = "SigntoolSign" + Parameters = @{ + OpusName = "VS Code" + OpusInfo = "https://code.visualstudio.com/" + PageHash = "/NPH" + TimeStamp = "/t `"http://ts4096.gtm.microsoft.com/TSS/AuthenticodeTS`"" + } + ToolName = "sign" + ToolVersion = "1.0" + }, + @{ + KeyCode = "CP-230012" + OperationCode = "SigntoolSign" + Parameters = @{ + OpusName = "VS Code" + OpusInfo = "https://code.visualstudio.com/" + Append = "/as" + FileDigest = "/fd `"SHA256`"" + PageHash = "/NPH" + TimeStamp = "/tr `"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer`" /td sha256" + } + ToolName = "sign" + ToolVersion = "1.0" + }, + @{ + KeyCode = "CP-230012" + OperationCode = "SigntoolVerify" + Parameters = @{ + VerifyAll = "/all" + } + ToolName = "sign" + ToolVersion = "1.0" + } + ) + } + } + ) +} + +$Output = [System.IO.Path]::GetTempFileName() +$ScriptPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +& "$ScriptPath\ESRPClient\packages\EsrpClient.1.0.27\tools\ESRPClient.exe" Sign -a $Auth -p $Policy -i $Input -o $Output \ No newline at end of file diff --git a/build/tsconfig.json b/build/tsconfig.json index d60805e7f77..b2cc8c8a0ab 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -6,6 +6,7 @@ "removeComments": false, "preserveConstEnums": true, "sourceMap": false, + "resolveJsonModule": true, "experimentalDecorators": true, // enable JavaScript type checking for the language service // use the tsconfig.build.json for compiling wich disable JavaScript diff --git a/build/win32/.gitignore b/build/win32/.gitignore new file mode 100644 index 00000000000..809f6a264e9 --- /dev/null +++ b/build/win32/.gitignore @@ -0,0 +1 @@ +code-processed.iss \ No newline at end of file diff --git a/build/win32/OSSREADME.json b/build/win32/OSSREADME.json old mode 100755 new mode 100644 index 5e1078b9519..0635d4a1e9f --- a/build/win32/OSSREADME.json +++ b/build/win32/OSSREADME.json @@ -547,7 +547,34 @@ }, { "name": "retep998/winapi-rs", - "version": "0.3.4", + "version": "0.1.1", + "repositoryUrl": "https://github.com/retep998/winapi-rs", + "licenseDetail": [ + "Copyright (c) 2015 The winapi-rs Developers", + "", + "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." + ], + "isProd": true + }, + { + "name": "retep998/winapi-rs", + "version": "0.2.2", "repositoryUrl": "https://github.com/retep998/winapi-rs", "licenseDetail": [ "Copyright (c) 2015 The winapi-rs Developers", @@ -601,7 +628,7 @@ }, { "name": "retep998/winapi-rs", - "version": "0.1.1", + "version": "0.3.4", "repositoryUrl": "https://github.com/retep998/winapi-rs", "licenseDetail": [ "Copyright (c) 2015 The winapi-rs Developers", @@ -680,33 +707,6 @@ ], "isProd": true }, - { - "name": "retep998/winapi-rs", - "version": "0.2.2", - "repositoryUrl": "https://github.com/retep998/winapi-rs", - "licenseDetail": [ - "Copyright (c) 2015 The winapi-rs Developers", - "", - "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." - ], - "isProd": true - }, { "name": "rust-lang-nursery/lazy-static.rs", "version": "1.0.0", diff --git a/build/win32/code.iss b/build/win32/code.iss index 9bc3329b03a..d9cf6e14dd7 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -11,7 +11,6 @@ AppPublisher=Microsoft Corporation AppPublisherURL=https://code.visualstudio.com/ AppSupportURL=https://code.visualstudio.com/ AppUpdatesURL=https://code.visualstudio.com/ -DefaultDirName={pf}\{#DirName} DefaultGroupName={#NameLong} AllowNoIcons=yes OutputDir={#OutputDir} @@ -33,9 +32,17 @@ VersionInfoVersion={#RawVersion} ShowLanguageDialog=auto ArchitecturesAllowed={#ArchitecturesAllowed} ArchitecturesInstallIn64BitMode={#ArchitecturesInstallIn64BitMode} +SignTool=esrp + +#if "user" == InstallTarget +DefaultDirName={userpf}\{#DirName} +PrivilegesRequired=lowest +#else +DefaultDirName={pf}\{#DirName} +#endif [Languages] -Name: "english"; MessagesFile: "compiler:Default.isl,{#RepoDir}\build\win32\i18n\messages.en.isl" {#LocalizedLanguageFile} +Name: "english"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.isl,{#RepoDir}\build\win32\i18n\messages.en.isl" {#LocalizedLanguageFile} Name: "german"; MessagesFile: "compiler:Languages\German.isl,{#RepoDir}\build\win32\i18n\messages.de.isl" {#LocalizedLanguageFile("deu")} Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl,{#RepoDir}\build\win32\i18n\messages.es.isl" {#LocalizedLanguageFile("esp")} Name: "french"; MessagesFile: "compiler:Languages\French.isl,{#RepoDir}\build\win32\i18n\messages.fr.isl" {#LocalizedLanguageFile("fra")} @@ -68,8 +75,9 @@ Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent [Files] -Source: "*"; Excludes: "\tools,\tools\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\resources\app\product.json"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion +Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: ignoreversion [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" @@ -81,851 +89,870 @@ Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent [Registry] -Root: HKCR; Subkey: ".ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.code"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".svgz\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".svgz\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svgz"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svgz"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVGZ}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svgz"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svgz\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.svgz\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: ".zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: ".zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles -Root: HKCR; Subkey: "{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCR; Subkey: "{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey -Root: HKCR; Subkey: "{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico" -Root: HKCR; Subkey: "{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" - -Root: HKCR; Subkey: "Applications\{#ExeBasename}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: HKCR; Subkey: "Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico" -Root: HKCR; Subkey: "Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" - -Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}\bin')) - -Root: HKCU; Subkey: "SOFTWARE\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles -Root: HKCU; Subkey: "SOFTWARE\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders -Root: HKCU; Subkey: "SOFTWARE\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders -Root: HKCU; Subkey: "SOFTWARE\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders -Root: HKCU; Subkey: "SOFTWARE\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders +#if "user" == InstallTarget +#define SoftwareClassesRootKey "HKCU" +#else +#define SoftwareClassesRootKey "HKLM" +#endif + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svgz\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svgz\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svgz"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVGZ}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svgz\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" + +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders + +; Environment +#if "user" == InstallTarget +#define EnvironmentRootKey "HKCU" +#define EnvironmentKey "Environment" +#define Uninstall64RootKey "HKCU64" +#define Uninstall32RootKey "HKCU32" +#else +#define EnvironmentRootKey "HKLM" +#define EnvironmentKey "System\CurrentControlSet\Control\Session Manager\Environment" +#define Uninstall64RootKey "HKLM64" +#define Uninstall32RootKey "HKLM32" +#endif + +Root: {#EnvironmentRootKey}; Subkey: "{#EnvironmentKey}"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}\bin')) [Code] // Don't allow installing conflicting architectures @@ -937,15 +964,33 @@ var begin Result := True; - if IsWin64 then begin - RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + copy('{#IncompatibleAppId}', 2, 38) + '_is1'; + #if "user" == InstallTarget + #if "ia32" == Arch + #define IncompatibleArchRootKey "HKLM32" + #else + #define IncompatibleArchRootKey "HKLM64" + #endif + + if not WizardSilent() then begin + RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + copy('{#IncompatibleTargetAppId}', 2, 38) + '_is1'; + + if RegKeyExists({#IncompatibleArchRootKey}, RegKey) then begin + if MsgBox('{#NameShort} is already installed on this system for all users. We recommend first uninstalling that version before installing this one. Are you sure you want to continue the installation?', mbConfirmation, MB_YESNO) = IDNO then begin + Result := false; + end; + end; + end; + #endif + + if Result and IsWin64 then begin + RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + copy('{#IncompatibleArchAppId}', 2, 38) + '_is1'; if '{#Arch}' = 'ia32' then begin - Result := not RegKeyExists(HKLM64, RegKey); + Result := not RegKeyExists({#Uninstall64RootKey}, RegKey); ThisArch := '32'; AltArch := '64'; end else begin - Result := not RegKeyExists(HKLM32, RegKey); + Result := not RegKeyExists({#Uninstall32RootKey}, RegKey); ThisArch := '64'; AltArch := '32'; end; @@ -1054,7 +1099,7 @@ function NeedsAddPath(Param: string): boolean; var OrigPath: string; begin - if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) + if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', OrigPath) then begin Result := True; exit; @@ -1073,7 +1118,7 @@ begin if not CurUninstallStep = usUninstall then begin exit; end; - if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Path) + if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', Path) then begin exit; end; @@ -1089,5 +1134,9 @@ begin end; end; end; - RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); + RegWriteExpandStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', NewPath); end; + +#ifdef Debug + #expr SaveToFile(AddBackslash(SourcePath) + "code-processed.iss") +#endif diff --git a/build/win32/i18n/Default.isl b/build/win32/i18n/Default.isl index fdcfbb16568..370da6b37c7 100644 --- a/build/win32/i18n/Default.isl +++ b/build/win32/i18n/Default.isl @@ -9,7 +9,7 @@ ; two periods being displayed). [LangOptions] -; The following three entries are very important. Be sure to read and +; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=English LanguageID=$0409 @@ -216,7 +216,7 @@ InstallingLabel=Please wait while Setup installs [name] on your computer. ; *** "Setup Completed" wizard page FinishedHeadingLabel=Completing the [name] Setup Wizard FinishedLabelNoIcons=Setup has finished installing [name] on your computer. -FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed shortcuts. +FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed icons. ClickFinish=Click Finish to exit Setup. FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now? FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now? @@ -296,7 +296,7 @@ UninstallNotFound=File "%1" does not exist. Cannot uninstall. UninstallOpenError=File "%1" could not be opened. Cannot uninstall UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log -ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? +ConfirmUninstall=Are you sure you want to completely remove %1? Extensions and settings will not be removed. UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows. OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges. UninstallStatusLabel=Please wait while %1 is removed from your computer. @@ -323,9 +323,9 @@ ShutdownBlockReasonUninstallingApp=Uninstalling %1. [CustomMessages] NameAndVersion=%1 version %2 -AdditionalIcons=Additional shortcuts: -CreateDesktopIcon=Create a &desktop shortcut -CreateQuickLaunchIcon=Create a &Quick Launch shortcut +AdditionalIcons=Additional icons: +CreateDesktopIcon=Create a &desktop icon +CreateQuickLaunchIcon=Create a &Quick Launch icon ProgramOnTheWeb=%1 on the Web UninstallProgram=Uninstall %1 LaunchProgram=Launch %1 @@ -334,4 +334,3 @@ AssocingFileExtension=Associating %1 with the %2 file extension... AutoStartProgramGroupDescription=Startup: AutoStartProgram=Automatically start %1 AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway? - diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe old mode 100755 new mode 100644 index 6d83b2b748b..9a2a7848ffe Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/build/yarn.lock b/build/yarn.lock index ac6150fcf60..e36dc85525d 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -652,9 +652,9 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" -typescript@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961" +typescript@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" underscore@1.8.3, underscore@~1.8.3: version "1.8.3" diff --git a/extensions/bat/syntaxes/batchfile.tmLanguage.json b/extensions/bat/syntaxes/batchfile.tmLanguage.json index e5f00ed3827..26ae88f43c5 100644 --- a/extensions/bat/syntaxes/batchfile.tmLanguage.json +++ b/extensions/bat/syntaxes/batchfile.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/mmims/language-batchfile/commit/6235c491be4dff49cd3966b50142874d7f79580a", + "version": "https://github.com/mmims/language-batchfile/commit/4b67596631b4ecd2c89c2ec1b2e08a6623438903", "name": "Batch File", "scopeName": "source.batchfile", "patterns": [ @@ -163,12 +163,6 @@ "end": "(?=$\\n|[&|><)])", "name": "meta.expression.set.batchfile", "patterns": [ - { - "include": "#command_set_inside_arithmetic" - }, - { - "include": "#command_set_group" - }, { "begin": "\"", "beginCaptures": { @@ -194,6 +188,12 @@ "include": "#variables" } ] + }, + { + "include": "#command_set_inside_arithmetic" + }, + { + "include": "#command_set_group" } ] }, @@ -266,8 +266,15 @@ "command_set_operators": { "patterns": [ { - "match": "\\+\\=|\\-\\=|\\*\\=|/\\=|%%\\=|&\\=|\\|\\=|\\^\\=|<<\\=|>>\\=", - "name": "keyword.operator.assignment.augmented.batchfile" + "match": "([^ ]*)(\\+\\=|\\-\\=|\\*\\=|\\/\\=|%%\\=|&\\=|\\|\\=|\\^\\=|<<\\=|>>\\=)", + "captures": { + "1": { + "name": "variable.other.readwrite.batchfile" + }, + "2": { + "name": "keyword.operator.assignment.augmented.batchfile" + } + } }, { "match": "\\+|\\-|/|\\*|%%|\\||&|\\^|<<|>>|~", @@ -278,8 +285,15 @@ "name": "keyword.operator.logical.batchfile" }, { - "match": "=", - "name": "keyword.operator.assignment.batchfile" + "match": "([^ ][^=]*)(=)", + "captures": { + "1": { + "name": "variable.other.readwrite.batchfile" + }, + "2": { + "name": "keyword.operator.assignment.batchfile" + } + } } ] }, diff --git a/extensions/bat/test/colorize-results/test_bat.json b/extensions/bat/test/colorize-results/test_bat.json index 3c2abc5d2dc..97155fafc8b 100644 --- a/extensions/bat/test/colorize-results/test_bat.json +++ b/extensions/bat/test/colorize-results/test_bat.json @@ -135,9 +135,9 @@ "c": "::", "t": "source.batchfile comment.line.colon.batchfile punctuation.definition.comment.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -146,9 +146,9 @@ "c": " Node modules", "t": "source.batchfile comment.line.colon.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -245,9 +245,9 @@ "c": "::", "t": "source.batchfile comment.line.colon.batchfile punctuation.definition.comment.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -256,9 +256,9 @@ "c": " Get electron", "t": "source.batchfile comment.line.colon.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -278,9 +278,9 @@ "c": "::", "t": "source.batchfile comment.line.colon.batchfile punctuation.definition.comment.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -289,9 +289,9 @@ "c": " Build", "t": "source.batchfile comment.line.colon.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -366,9 +366,9 @@ "c": "::", "t": "source.batchfile comment.line.colon.batchfile punctuation.definition.comment.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -377,9 +377,9 @@ "c": " Configuration", "t": "source.batchfile comment.line.colon.batchfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/clojure/test/colorize-results/test_clj.json b/extensions/clojure/test/colorize-results/test_clj.json index 8a704fb9683..b7dd17d91e8 100644 --- a/extensions/clojure/test/colorize-results/test_clj.json +++ b/extensions/clojure/test/colorize-results/test_clj.json @@ -3,9 +3,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -14,9 +14,9 @@ "c": "; from http://clojure-doc.org/articles/tutorials/introduction.html", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -311,9 +311,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -322,9 +322,9 @@ "c": " A vector", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -905,9 +905,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -916,9 +916,9 @@ "c": " this is more typical usage.", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1433,9 +1433,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1444,9 +1444,9 @@ "c": "; ⇒ (+ 1 2 3)", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1840,9 +1840,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1851,9 +1851,9 @@ "c": "; Vectors", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2236,9 +2236,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2247,9 +2247,9 @@ "c": " ⇒ [:a :b :c :d]", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2346,9 +2346,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2357,9 +2357,9 @@ "c": " ⇒ (:d :a :b :c)", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2390,9 +2390,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2401,9 +2401,9 @@ "c": " ⇒ is still [:a :b :c]", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2434,9 +2434,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2445,9 +2445,9 @@ "c": " ⇒ is still (:a :b :c)", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2456,9 +2456,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2467,9 +2467,9 @@ "c": "; Maps", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2753,9 +2753,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2764,9 +2764,9 @@ "c": " ⇒ {:a 1 :c 3 :b 2}", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2863,9 +2863,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -2874,9 +2874,9 @@ "c": " ⇒ {:a 1}", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3050,9 +3050,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3061,9 +3061,9 @@ "c": "; ⇒ #'user/my-atom", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3094,9 +3094,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3105,9 +3105,9 @@ "c": "; ⇒ {:foo 1}", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3259,9 +3259,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3270,9 +3270,9 @@ "c": "; ⇒ {:foo 2}", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3303,9 +3303,9 @@ "c": ";", "t": "source.clojure comment.line.semicolon.clojure punctuation.definition.comment.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -3314,9 +3314,9 @@ "c": "; ⇒ {:foo 2}", "t": "source.clojure comment.line.semicolon.clojure", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/coffeescript/test/colorize-results/test-regex_coffee.json b/extensions/coffeescript/test/colorize-results/test-regex_coffee.json index ad11ba9d687..9daab0d5533 100644 --- a/extensions/coffeescript/test/colorize-results/test-regex_coffee.json +++ b/extensions/coffeescript/test/colorize-results/test-regex_coffee.json @@ -575,9 +575,9 @@ "c": "#", "t": "source.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -586,9 +586,9 @@ "c": " numbers", "t": "source.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -663,9 +663,9 @@ "c": "#", "t": "source.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -674,9 +674,9 @@ "c": " letters", "t": "source.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -696,9 +696,9 @@ "c": "#", "t": "source.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -707,9 +707,9 @@ "c": " the end", "t": "source.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/coffeescript/test/colorize-results/test_coffee.json b/extensions/coffeescript/test/colorize-results/test_coffee.json index e7eae7d047f..d3de07d3f82 100644 --- a/extensions/coffeescript/test/colorize-results/test_coffee.json +++ b/extensions/coffeescript/test/colorize-results/test_coffee.json @@ -1433,9 +1433,9 @@ "c": "#", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1444,9 +1444,9 @@ "c": " numbers", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1521,9 +1521,9 @@ "c": "#", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1532,9 +1532,9 @@ "c": " letters", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1576,9 +1576,9 @@ "c": "#", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee punctuation.definition.comment.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1587,9 +1587,9 @@ "c": " the end", "t": "source.coffee string.regexp.multiline.coffee comment.line.number-sign.coffee", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/configuration-editing/extension.webpack.config.js b/extensions/configuration-editing/extension.webpack.config.js new file mode 100644 index 00000000000..b474e65cbb1 --- /dev/null +++ b/extensions/configuration-editing/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + resolve: { + mainFields: ['module', 'main'] + } +}); diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index e66a313c9f6..46f4eba3a95 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -17,8 +17,8 @@ "watch": "gulp watch-extension:configuration-editing" }, "dependencies": { - "jsonc-parser": "^1.0.0", - "vscode-nls": "^3.2.1" + "jsonc-parser": "2.0.2", + "vscode-nls": "^4.0.0" }, "contributes": { "languages": [ @@ -96,6 +96,6 @@ ] }, "devDependencies": { - "@types/node": "7.0.4" + "@types/node": "^8.10.25" } -} \ No newline at end of file +} diff --git a/extensions/configuration-editing/package.nls.json b/extensions/configuration-editing/package.nls.json index b8c247a9de3..20a9c1af8b7 100644 --- a/extensions/configuration-editing/package.nls.json +++ b/extensions/configuration-editing/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Configuration Editing", - "description": "Provides capabilities (advanced IntelliSense, auto-fixing) in configuration files like settings, launch and extension recommendation files." + "description": "Provides capabilities (advanced IntelliSense, auto-fixing) in configuration files like settings, launch, and extension recommendation files." } \ No newline at end of file diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index 68dbbf165b4..f6ec0b625e7 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -11,8 +11,8 @@ import { getLocation, visit, parse, ParseErrorCode } from 'jsonc-parser'; import * as path from 'path'; import { SettingsDocument } from './settingsDocumentHelper'; -const decoration = vscode.window.createTextEditorDecorationType({ - color: '#9e9e9e' +const fadedDecoration = vscode.window.createTextEditorDecorationType({ + color: '#777' }); let pendingLaunchJsonDecoration: NodeJS.Timer; @@ -241,7 +241,7 @@ function updateLaunchJsonDecorations(editor: vscode.TextEditor | undefined): voi } }); - editor.setDecorations(decoration, ranges); + editor.setDecorations(fadedDecoration, ranges); } vscode.languages.registerDocumentSymbolProvider({ pattern: '**/launch.json', language: 'jsonc' }, { diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 29d3d43ae80..c74f1de37ba 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -2,14 +2,14 @@ # yarn lockfile v1 -"@types/node@7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.4.tgz#9aabc135979ded383325749f508894c662948c8b" +"@types/node@^8.10.25": + version "8.10.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.25.tgz#801fe4e39372cef18f268db880a5fbfcf71adc7e" -jsonc-parser@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.0.tgz#ddcc864ae708e60a7a6dd36daea00172fa8d9272" +jsonc-parser@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" -vscode-nls@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" diff --git a/extensions/cpp/test/colorize-results/test_c.json b/extensions/cpp/test/colorize-results/test_c.json index d3bac881c43..0725010d8c2 100644 --- a/extensions/cpp/test/colorize-results/test_c.json +++ b/extensions/cpp/test/colorize-results/test_c.json @@ -3,9 +3,9 @@ "c": "/*", "t": "source.c comment.block.c punctuation.definition.comment.begin.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -14,9 +14,9 @@ "c": " C Program to find roots of a quadratic equation when coefficients are entered by user. ", "t": "source.c comment.block.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -25,9 +25,9 @@ "c": "*/", "t": "source.c comment.block.c punctuation.definition.comment.end.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -36,9 +36,9 @@ "c": "/*", "t": "source.c comment.block.c punctuation.definition.comment.begin.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -47,9 +47,9 @@ "c": " Library function sqrt() computes the square root. ", "t": "source.c comment.block.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -58,9 +58,9 @@ "c": "*/", "t": "source.c comment.block.c punctuation.definition.comment.end.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -212,9 +212,9 @@ "c": "/*", "t": "source.c comment.block.c punctuation.definition.comment.begin.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -223,9 +223,9 @@ "c": " This is needed to use sqrt() function.", "t": "source.c comment.block.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -234,9 +234,9 @@ "c": "*/", "t": "source.c comment.block.c punctuation.definition.comment.end.c", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/cpp/test/colorize-results/test_cc.json b/extensions/cpp/test/colorize-results/test_cc.json index 845a693b3ab..f3f72320fb5 100644 --- a/extensions/cpp/test/colorize-results/test_cc.json +++ b/extensions/cpp/test/colorize-results/test_cc.json @@ -1114,9 +1114,9 @@ "c": "//", "t": "source.cpp meta.block.c comment.line.double-slash.cpp punctuation.definition.comment.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1125,9 +1125,9 @@ "c": " everything from this point on is interpeted as a string literal...", "t": "source.cpp meta.block.c comment.line.double-slash.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1312,9 +1312,9 @@ "c": "//", "t": "source.cpp meta.block.c comment.line.double-slash.cpp punctuation.definition.comment.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1323,9 +1323,9 @@ "c": " sadness.", "t": "source.cpp meta.block.c comment.line.double-slash.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1708,9 +1708,9 @@ "c": "//", "t": "source.cpp meta.block.c comment.line.double-slash.cpp punctuation.definition.comment.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1719,9 +1719,9 @@ "c": " the rest of", "t": "source.cpp meta.block.c comment.line.double-slash.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1961,9 +1961,9 @@ "c": "//", "t": "source.cpp meta.block.c comment.line.double-slash.cpp punctuation.definition.comment.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1972,9 +1972,9 @@ "c": " the rest of", "t": "source.cpp meta.block.c comment.line.double-slash.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/cpp/test/colorize-results/test_cpp.json b/extensions/cpp/test/colorize-results/test_cpp.json index 8527e98a4f2..b3c9a841cc4 100644 --- a/extensions/cpp/test/colorize-results/test_cpp.json +++ b/extensions/cpp/test/colorize-results/test_cpp.json @@ -3,9 +3,9 @@ "c": "//", "t": "source.cpp comment.line.double-slash.cpp punctuation.definition.comment.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -14,9 +14,9 @@ "c": " classes example", "t": "source.cpp comment.line.double-slash.cpp", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/csharp/test/colorize-results/test_cs.json b/extensions/csharp/test/colorize-results/test_cs.json index 6b56bff9935..1fc73fb341c 100644 --- a/extensions/csharp/test/colorize-results/test_cs.json +++ b/extensions/csharp/test/colorize-results/test_cs.json @@ -1114,9 +1114,9 @@ "c": "//", "t": "source.cs comment.line.double-slash.cs punctuation.definition.comment.cs", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -1125,9 +1125,9 @@ "c": " Display the number of command line arguments:", "t": "source.cs comment.line.double-slash.cs", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/css-language-features/.vscode/launch.json b/extensions/css-language-features/.vscode/launch.json index f6f6922c78d..d6393141c5d 100644 --- a/extensions/css-language-features/.vscode/launch.json +++ b/extensions/css-language-features/.vscode/launch.json @@ -3,7 +3,10 @@ "compounds": [ { "name": "Debug Extension and Language Server", - "configurations": ["Launch Extension", "Attach Language Server"] + "configurations": [ + "Launch Extension", + "Attach Language Server" + ] } ], "configurations": [ @@ -17,7 +20,9 @@ ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/**/*.js"], + "outFiles": [ + "${workspaceFolder}/client/out/**/*.js" + ], "smartStep": true, "preLaunchTask": "npm: compile" }, @@ -26,10 +31,15 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/client/out/test" ], + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/client/out/test" + ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/test/**/*.js"], + "outFiles": [ + "${workspaceFolder}/client/out/test/**/*.js" + ], "preLaunchTask": "npm: compile" }, { @@ -39,9 +49,31 @@ "protocol": "inspector", "port": 6044, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/server/out/**/*.js"], + "outFiles": [ + "${workspaceFolder}/server/out/**/*.js" + ], "smartStep": true, "restart": true + }, + { + "name": "Server Unit Tests", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "args": [ + "--timeout", + "999999", + "--colors" + ], + "cwd": "${workspaceRoot}", + "runtimeExecutable": null, + "runtimeArgs": [], + "env": {}, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/server/out/**" + ] } ] } \ No newline at end of file diff --git a/extensions/css-language-features/.vscodeignore b/extensions/css-language-features/.vscodeignore index b85541d78ee..89a62ee0761 100644 --- a/extensions/css-language-features/.vscodeignore +++ b/extensions/css-language-features/.vscodeignore @@ -1,5 +1,21 @@ +test/** +.vscode/** +server/.vscode/** +node_modules/** +server/node_modules/** client/src/** -client/tsconfig.json server/src/** +client/out/** +server/out/** +client/tsconfig.json server/tsconfig.json -server/node_modules/@types/** \ No newline at end of file +server/test/** +server/bin/** +server/build/** +server/yarn.lock +server/.npmignore +yarn.lock +server/extension.webpack.config.js +extension.webpack.config.js +!node_modules/vscode-nls/** +!server/node_modules/vscode-nls/** \ No newline at end of file diff --git a/extensions/css-language-features/CONTRIBUTING.md b/extensions/css-language-features/CONTRIBUTING.md new file mode 100644 index 00000000000..38843f2fbaa --- /dev/null +++ b/extensions/css-language-features/CONTRIBUTING.md @@ -0,0 +1,39 @@ + +## Setup + +- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) +- Run `yarn` at `/`, this will install + - Dependencies for `/extension/css-language-features/` + - Dependencies for `/extension/css-language-features/server/` + - devDependencies such as `gulp` +- Open `/extensions/css-language-features/` as the workspace in VS Code +- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/.vscode/launch.json) debug target in the Debug View. This will: + - Launch the `preLaunchTask` task to compile the extension + - Launch a new VS Code instance with the `css-language-features` extension loaded + - You should see a notification saying the development version of `css-language-features` overwrites the bundled version of `css-language-features` +- Test the behavior of this extension by editing CSS/SCSS/Less files +- Run `Reload Window` command in the launched instance to reload the extension + +### Contribute to vscode-css-languageservice + +[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less. +This extension wraps the css language service into a Language Server for VS Code. +If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). + +However, within this extension, you can run a development version of `vscode-css-languageservice` to debug code or test language features interactively: + +#### Linking `vscode-css-languageservice` in `css-language-features/server/` + +- Clone [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) +- Run `yarn` in `vscode-css-languageservice` +- Run `yarn link` in `vscode-css-languageservice`. This will compile and link `vscode-css-languageservice` +- In `css-language-features/server/`, run `yarn link vscode-css-languageservice` + +#### Testing the development version of `vscode-css-languageservice` + +- Open both `vscode-css-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature +- Run `yarn watch` in `vscode-css-languageservice` to recompile the extension whenever it changes +- Run `yarn watch` at `css-language-features/server/` to recompile this extension with the linked version of `vscode-css-languageservice` +- Make some changes in `vscode-css-languageservice` +- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-css-languageservice`. You can interactively test the language features. +- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-css-languageservice` and `css-language-features/server/` diff --git a/extensions/css-language-features/README.md b/extensions/css-language-features/README.md index 43065919fd4..5a3fad4948b 100644 --- a/extensions/css-language-features/README.md +++ b/extensions/css-language-features/README.md @@ -1,35 +1,9 @@ -# CSS Language Features +# Language Features for CSS, SCSS, and LESS files -This extension offers CSS/SCSS/Less support in VS Code. +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. -## Development +## Features -- Clone [Microsoft/vscode](https://github.com/microsoft/vscode) -- Run `yarn` at `/`, this will install - - Dependencies for `/extension/css-language-features/` - - Dependencies for `/extension/css-language-features/server/` - - devDependencies such as `gulp` -- Open `/extensions/css-language-features/` as the workspace in VS Code -- Run the [`Launch Extension`](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/.vscode/launch.json) debug target in the Debug View. This will: - - Launch the `preLaunchTask` task to compile the extension - - Launch a new VS Code instance with the `css-language-features` extension loaded - - You should see a notification saying the development version of `css-language-features` overwrites the bundled version of `css-language-features` -- Test the behavior of this extension by editing CSS/SCSS/Less files -- Run `Reload Window` command in the launched instance to reload the extension +See [CSS, SCSS and Less in VS Code](https://code.visualstudio.com/docs/languages/css) to learn about the features of this extension. -### Contribute to vscode-css-languageservice - -[Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) contains the language smarts for CSS/SCSS/Less, and this extension wraps the service into a Language Server for VS Code. If you want to fix CSS/SCSS/Less issues or make improvements, you should make changes at [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice). - -However, within this extension, you can run a development version of `vscode-css-languageservice` to debug code or test language features interactively: - -- Clone [Microsoft/vscode-css-languageservice](https://github.com/Microsoft/vscode-css-languageservice) -- Open both `vscode-css-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature -- Run `yarn` in `vscode-css-languageservice` -- Run `yarn link` in `vscode-css-languageservice`. This will compile and link `vscode-css-languageservice` -- In `css-language-features/server/`, run `npm link vscode-css-languageservice` -- Run `yarn watch` at `css-languagefeatures/server/` to recompile this extension with the linked version of `vscode-css-languageservice` -- Run `yarn watch` in `vscode-css-languageservice` -- Make some changes in `vscode-css-languageservice` -- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-css-languageservice`. You can interactively test the language features. -- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-css-languageservice` and `css-language-features/server/` +Please read the [CONTRIBUTING.md](https://github.com/Microsoft/vscode/blob/master/extensions/css-language-features/CONTRIBUTING.md) file to learn how to contribute to this extension. \ 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 a7a031bda3c..e91d5408054 100644 --- a/extensions/css-language-features/client/src/cssMain.ts +++ b/extensions/css-language-features/client/src/cssMain.ts @@ -4,19 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as path from 'path'; +import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { languages, window, commands, ExtensionContext, Range, Position, TextDocument, CompletionItem, CompletionItemKind, TextEdit, SnippetString, FoldingRangeKind, FoldingRange, FoldingContext, CancellationToken } from 'vscode'; +import { languages, window, commands, ExtensionContext, Range, Position, CompletionItem, CompletionItemKind, TextEdit, SnippetString } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Disposable } from 'vscode-languageclient'; -import { FoldingRangeRequest, FoldingRangeRequestParam, FoldingRangeClientCapabilities, FoldingRangeKind as LSFoldingRangeKind } from 'vscode-languageserver-protocol-foldingprovider'; // this method is called when vs code is activated export function activate(context: ExtensionContext) { - // The server is implemented in node - let serverModule = context.asAbsolutePath(path.join('server', 'out', 'cssServerMain.js')); + let serverMain = readJSONFile(context.asAbsolutePath('./server/package.json')).main; + let serverModule = context.asAbsolutePath(path.join('server', serverMain)); + // The debug options for the server let debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] }; @@ -42,21 +43,6 @@ export function activate(context: ExtensionContext) { // Create the language client and start the client. let client = new LanguageClient('css', localize('cssserver.name', 'CSS Language Server'), serverOptions, clientOptions); client.registerProposedFeatures(); - client.registerFeature({ - fillClientCapabilities(capabilities: FoldingRangeClientCapabilities): void { - let textDocumentCap = capabilities.textDocument; - if (!textDocumentCap) { - textDocumentCap = capabilities.textDocument = {}; - } - textDocumentCap.foldingRange = { - dynamicRegistration: false, - rangeLimit: 5000, - lineFoldingOnly: true - }; - }, - initialize(capabilities, documentSelector): void { - } - }); let disposable = client.start(); // Push the disposable to the context's subscriptions so that the @@ -85,7 +71,6 @@ export function activate(context: ExtensionContext) { client.onReady().then(() => { context.subscriptions.push(initCompletionProvider()); - context.subscriptions.push(initFoldingProvider()); }); function initCompletionProvider(): Disposable { @@ -116,38 +101,6 @@ export function activate(context: ExtensionContext) { }); } - function initFoldingProvider(): Disposable { - function getKind(kind: string | undefined): FoldingRangeKind | undefined { - if (kind) { - switch (kind) { - case LSFoldingRangeKind.Comment: - return FoldingRangeKind.Comment; - case LSFoldingRangeKind.Imports: - return FoldingRangeKind.Imports; - case LSFoldingRangeKind.Region: - return FoldingRangeKind.Region; - } - } - return void 0; - } - return languages.registerFoldingRangeProvider(documentSelector, { - provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken) { - const param: FoldingRangeRequestParam = { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) - }; - return client.sendRequest(FoldingRangeRequest.type, param, token).then(ranges => { - if (Array.isArray(ranges)) { - return ranges.map(r => new FoldingRange(r.startLine, r.endLine, getKind(r.kind))); - } - return null; - }, error => { - client.logFailedRequest(FoldingRangeRequest.type, error); - return null; - }); - } - }); - } - commands.registerCommand('_css.applyCodeAction', applyCodeAction); function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) { @@ -169,3 +122,12 @@ export function activate(context: ExtensionContext) { } } +function readJSONFile(location: string) { + try { + return JSON.parse(fs.readFileSync(location).toString()); + } catch (e) { + console.log(`Problems reading ${location}: ${e}`); + return {}; + } +} + diff --git a/extensions/css-language-features/extension.webpack.config.js b/extensions/css-language-features/extension.webpack.config.js new file mode 100644 index 00000000000..3c1d55ef6a0 --- /dev/null +++ b/extensions/css-language-features/extension.webpack.config.js @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const path = require('path'); + +module.exports = withDefaults({ + context: path.join(__dirname, 'client'), + entry: { + extension: './src/cssMain.ts', + }, + resolve: { + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'] // support ts-files and js-files + }, + output: { + filename: 'cssMain.js', + path: path.join(__dirname, 'client', 'dist'), + libraryTarget: "commonjs", + }, + externals: { + './files': 'commonjs', // ignored because it doesn't exist + }, + plugins: [ + new CopyWebpackPlugin([ + { from: './out/*.sh', to: '[name].sh' }, + { from: './out/nls.*.json', to: '[name].json' } + ]) + ] +}); diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index b3e9af99b7b..a565f4309ef 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -19,6 +19,7 @@ "scripts": { "compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", "watch": "gulp watch-extension:css-language-features-client watch-extension:css-language-features-server", + "test": "mocha", "postinstall": "cd server && yarn install", "install-client-next": "yarn add vscode-languageclient@next" }, @@ -109,7 +110,7 @@ "error" ], "default": "ignore", - "description": "%css.lint.boxModel.desc%" + "markdownDescription": "%css.lint.boxModel.desc%" }, "css.lint.universalSelector": { "type": "string", @@ -120,7 +121,7 @@ "error" ], "default": "ignore", - "description": "%css.lint.universalSelector.desc%" + "markdownDescription": "%css.lint.universalSelector.desc%" }, "css.lint.zeroUnits": { "type": "string", @@ -208,7 +209,7 @@ "error" ], "default": "warning", - "description": "%css.lint.propertyIgnoredDueToDisplay.desc%" + "markdownDescription": "%css.lint.propertyIgnoredDueToDisplay.desc%" }, "css.lint.important": { "type": "string", @@ -230,7 +231,7 @@ "error" ], "default": "ignore", - "description": "%css.lint.float.desc%" + "markdownDescription": "%css.lint.float.desc%" }, "css.lint.idSelector": { "type": "string", @@ -243,6 +244,17 @@ "default": "ignore", "description": "%css.lint.idSelector.desc%" }, + "css.lint.unknownAtRules": { + "type": "string", + "scope": "resource", + "enum": [ + "ignore", + "warning", + "error" + ], + "default": "warning", + "description": "%css.lint.unknownAtRules.desc%" + }, "css.trace.server": { "type": "string", "scope": "window", @@ -338,7 +350,7 @@ "error" ], "default": "ignore", - "description": "%scss.lint.boxModel.desc%" + "markdownDescription": "%scss.lint.boxModel.desc%" }, "scss.lint.universalSelector": { "type": "string", @@ -349,7 +361,7 @@ "error" ], "default": "ignore", - "description": "%scss.lint.universalSelector.desc%" + "markdownDescription": "%scss.lint.universalSelector.desc%" }, "scss.lint.zeroUnits": { "type": "string", @@ -371,7 +383,7 @@ "error" ], "default": "warning", - "description": "%scss.lint.fontFaceProperties.desc%" + "markdownDescription": "%scss.lint.fontFaceProperties.desc%" }, "scss.lint.hexColorLength": { "type": "string", @@ -437,7 +449,7 @@ "error" ], "default": "warning", - "description": "%scss.lint.propertyIgnoredDueToDisplay.desc%" + "markdownDescription": "%scss.lint.propertyIgnoredDueToDisplay.desc%" }, "scss.lint.important": { "type": "string", @@ -448,7 +460,7 @@ "error" ], "default": "ignore", - "description": "%scss.lint.important.desc%" + "markdownDescription": "%scss.lint.important.desc%" }, "scss.lint.float": { "type": "string", @@ -459,7 +471,7 @@ "error" ], "default": "ignore", - "description": "%scss.lint.float.desc%" + "markdownDescription": "%scss.lint.float.desc%" }, "scss.lint.idSelector": { "type": "string", @@ -557,7 +569,7 @@ "error" ], "default": "ignore", - "description": "%less.lint.boxModel.desc%" + "markdownDescription": "%less.lint.boxModel.desc%" }, "less.lint.universalSelector": { "type": "string", @@ -568,7 +580,7 @@ "error" ], "default": "ignore", - "description": "%less.lint.universalSelector.desc%" + "markdownDescription": "%less.lint.universalSelector.desc%" }, "less.lint.zeroUnits": { "type": "string", @@ -590,7 +602,7 @@ "error" ], "default": "warning", - "description": "%less.lint.fontFaceProperties.desc%" + "markdownDescription": "%less.lint.fontFaceProperties.desc%" }, "less.lint.hexColorLength": { "type": "string", @@ -656,7 +668,7 @@ "error" ], "default": "warning", - "description": "%less.lint.propertyIgnoredDueToDisplay.desc%" + "markdownDescription": "%less.lint.propertyIgnoredDueToDisplay.desc%" }, "less.lint.important": { "type": "string", @@ -678,7 +690,7 @@ "error" ], "default": "ignore", - "description": "%less.lint.float.desc%" + "markdownDescription": "%less.lint.float.desc%" }, "less.lint.idSelector": { "type": "string", @@ -696,11 +708,11 @@ ] }, "dependencies": { - "vscode-languageclient": "^4.1.4", - "vscode-languageserver-protocol-foldingprovider": "^2.0.1", - "vscode-nls": "^3.2.2" + "vscode-languageclient": "^5.1.0-next.9", + "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "7.0.43" + "@types/node": "^8.10.25", + "mocha": "^5.2.0" } } diff --git a/extensions/css-language-features/package.nls.json b/extensions/css-language-features/package.nls.json index cbe77ea0abb..f62c6fb669c 100644 --- a/extensions/css-language-features/package.nls.json +++ b/extensions/css-language-features/package.nls.json @@ -2,72 +2,73 @@ "displayName": "CSS Language Features", "description": "Provides rich language support for CSS, LESS and SCSS files.", "css.title": "CSS", - "css.lint.argumentsInColorFunction.desc": "Invalid number of parameters", - "css.lint.boxModel.desc": "Do not use width or height when using padding or border", - "css.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties", - "css.lint.duplicateProperties.desc": "Do not use duplicate style definitions", - "css.lint.emptyRules.desc": "Do not use empty rulesets", - "css.lint.float.desc": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", - "css.lint.fontFaceProperties.desc": "@font-face rule must define 'src' and 'font-family' properties", - "css.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers", + "css.lint.argumentsInColorFunction.desc": "Invalid number of parameters.", + "css.lint.boxModel.desc": "Do not use `width` or `height` when using `padding` or `border`.", + "css.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties.", + "css.lint.duplicateProperties.desc": "Do not use duplicate style definitions.", + "css.lint.emptyRules.desc": "Do not use empty rulesets.", + "css.lint.float.desc": "Avoid using `float`. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", + "css.lint.fontFaceProperties.desc": "`@font-face` rule must define `src` and `font-family` properties.", + "css.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers.", "css.lint.idSelector.desc": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML.", - "css.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older", - "css.lint.important.desc": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored.", - "css.lint.importStatement.desc": "Import statements do not load in parallel", - "css.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect", - "css.lint.universalSelector.desc": "The universal selector (*) is known to be slow", + "css.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older.", + "css.lint.important.desc": "Avoid using `!important`. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored.", + "css.lint.importStatement.desc": "Import statements do not load in parallel.", + "css.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with `display: inline`, the `width`, `height`, `margin-top`, `margin-bottom`, and `float` properties have no effect.", + "css.lint.universalSelector.desc": "The universal selector (`*`) is known to be slow.", + "css.lint.unknownAtRules.desc": "Unknown at-rule.", "css.lint.unknownProperties.desc": "Unknown property.", "css.lint.unknownVendorSpecificProperties.desc": "Unknown vendor specific property.", - "css.lint.vendorPrefix.desc": "When using a vendor-specific prefix also include the standard property", - "css.lint.zeroUnits.desc": "No unit for zero needed", + "css.lint.vendorPrefix.desc": "When using a vendor-specific prefix, also include the standard property.", + "css.lint.zeroUnits.desc": "No unit for zero needed.", "css.trace.server.desc": "Traces the communication between VS Code and the CSS language server.", "css.validate.title": "Controls CSS validation and problem severities.", - "css.validate.desc": "Enables or disables all validations", + "css.validate.desc": "Enables or disables all validations.", "less.title": "LESS", - "less.lint.argumentsInColorFunction.desc": "Invalid number of parameters", - "less.lint.boxModel.desc": "Do not use width or height when using padding or border", - "less.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties", - "less.lint.duplicateProperties.desc": "Do not use duplicate style definitions", - "less.lint.emptyRules.desc": "Do not use empty rulesets", - "less.lint.float.desc": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", - "less.lint.fontFaceProperties.desc": "@font-face rule must define 'src' and 'font-family' properties", - "less.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers", + "less.lint.argumentsInColorFunction.desc": "Invalid number of parameters.", + "less.lint.boxModel.desc": "Do not use `width` or `height` when using `padding` or `border`.", + "less.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties.", + "less.lint.duplicateProperties.desc": "Do not use duplicate style definitions.", + "less.lint.emptyRules.desc": "Do not use empty rulesets.", + "less.lint.float.desc": "Avoid using `float`. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", + "less.lint.fontFaceProperties.desc": "`@font-face` rule must define `src` and `font-family` properties.", + "less.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers.", "less.lint.idSelector.desc": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML.", - "less.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older", + "less.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older.", "less.lint.important.desc": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored.", - "less.lint.importStatement.desc": "Import statements do not load in parallel", - "less.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect", - "less.lint.universalSelector.desc": "The universal selector (*) is known to be slow", + "less.lint.importStatement.desc": "Import statements do not load in parallel.", + "less.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with `display: inline`, the `width`, `height`, `margin-top`, `margin-bottom`, and `float` properties have no effect.", + "less.lint.universalSelector.desc": "The universal selector (`*`) is known to be slow.", "less.lint.unknownProperties.desc": "Unknown property.", "less.lint.unknownVendorSpecificProperties.desc": "Unknown vendor specific property.", - "less.lint.vendorPrefix.desc": "When using a vendor-specific prefix also include the standard property", - "less.lint.zeroUnits.desc": "No unit for zero needed", + "less.lint.vendorPrefix.desc": "When using a vendor-specific prefix, also include the standard property.", + "less.lint.zeroUnits.desc": "No unit for zero needed.", "less.validate.title": "Controls LESS validation and problem severities.", - "less.validate.desc": "Enables or disables all validations", + "less.validate.desc": "Enables or disables all validations.", "scss.title": "SCSS (Sass)", - "scss.lint.argumentsInColorFunction.desc": "Invalid number of parameters", - "scss.lint.boxModel.desc": "Do not use width or height when using padding or border", - "scss.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties", - "scss.lint.duplicateProperties.desc": "Do not use duplicate style definitions", - "scss.lint.emptyRules.desc": "Do not use empty rulesets", - "scss.lint.float.desc": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", - "scss.lint.fontFaceProperties.desc": "@font-face rule must define 'src' and 'font-family' properties", - "scss.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers", + "scss.lint.argumentsInColorFunction.desc": "Invalid number of parameters.", + "scss.lint.boxModel.desc": "Do not use `width` or `height` when using `padding` or `border`.", + "scss.lint.compatibleVendorPrefixes.desc": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties.", + "scss.lint.duplicateProperties.desc": "Do not use duplicate style definitions.", + "scss.lint.emptyRules.desc": "Do not use empty rulesets.", + "scss.lint.float.desc": "Avoid using `float`. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes.", + "scss.lint.fontFaceProperties.desc": "`@font-face` rule must define `src` and `font-family` properties.", + "scss.lint.hexColorLength.desc": "Hex colors must consist of three or six hex numbers.", "scss.lint.idSelector.desc": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML.", - "scss.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older", + "scss.lint.ieHack.desc": "IE hacks are only necessary when supporting IE7 and older.", "scss.lint.important.desc": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored.", - "scss.lint.importStatement.desc": "Import statements do not load in parallel", - "scss.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect", - "scss.lint.universalSelector.desc": "The universal selector (*) is known to be slow", + "scss.lint.importStatement.desc": "Import statements do not load in parallel.", + "scss.lint.propertyIgnoredDueToDisplay.desc": "Property is ignored due to the display. E.g. with `display: inline`, the `width`, `height`, `margin-top`, `margin-bottom`, and `float` properties have no effect.", + "scss.lint.universalSelector.desc": "The universal selector (`*`) is known to be slow.", "scss.lint.unknownProperties.desc": "Unknown property.", "scss.lint.unknownVendorSpecificProperties.desc": "Unknown vendor specific property.", - "scss.lint.vendorPrefix.desc": "When using a vendor-specific prefix also include the standard property", - "scss.lint.zeroUnits.desc": "No unit for zero needed", + "scss.lint.vendorPrefix.desc": "When using a vendor-specific prefix, also include the standard property.", + "scss.lint.zeroUnits.desc": "No unit for zero needed.", "scss.validate.title": "Controls SCSS validation and problem severities.", - "scss.validate.desc": "Enables or disables all validations", - "less.colorDecorators.enable.desc": "Enables or disables color decorators", - "scss.colorDecorators.enable.desc": "Enables or disables color decorators", - "css.colorDecorators.enable.desc": "Enables or disables color decorators", + "scss.validate.desc": "Enables or disables all validations.", + "less.colorDecorators.enable.desc": "Enables or disables color decorators.", + "scss.colorDecorators.enable.desc": "Enables or disables color decorators.", + "css.colorDecorators.enable.desc": "Enables or disables color decorators.", "css.colorDecorators.enable.deprecationMessage": "The setting `css.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", "scss.colorDecorators.enable.deprecationMessage": "The setting `scss.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", "less.colorDecorators.enable.deprecationMessage": "The setting `less.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`." diff --git a/extensions/css-language-features/server/build/filesFillIn.js b/extensions/css-language-features/server/build/filesFillIn.js new file mode 100644 index 00000000000..906617384e0 --- /dev/null +++ b/extensions/css-language-features/server/build/filesFillIn.js @@ -0,0 +1,5 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = {}; \ No newline at end of file diff --git a/extensions/css-language-features/server/extension.webpack.config.js b/extensions/css-language-features/server/extension.webpack.config.js new file mode 100644 index 00000000000..29533fa59fb --- /dev/null +++ b/extensions/css-language-features/server/extension.webpack.config.js @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../../shared.webpack.config'); +const path = require('path'); +var webpack = require('webpack'); + +module.exports = withDefaults({ + context: path.join(__dirname), + entry: { + extension: './src/cssServerMain.ts', + }, + resolve: { + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'] // support ts-files and js-files + }, + output: { + filename: 'cssServerMain.js', + path: path.join(__dirname, 'dist'), + libraryTarget: "commonjs", + }, + externals: { + "vscode-nls": 'commonjs vscode-nls', + }, + plugins: [ + new webpack.NormalModuleReplacementPlugin( + /(\/|\\)vscode-languageserver(\/|\\)lib(\/|\\)files\.js/, + require.resolve('./build/filesFillIn') + ), + new webpack.IgnorePlugin(/vertx/) + ], +}); diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 9404a08ce70..0ef07147fe0 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -7,14 +7,18 @@ "engines": { "node": "*" }, + "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^3.0.9-next.18", - "vscode-languageserver": "^4.1.3", - "vscode-languageserver-protocol-foldingprovider": "^2.0.1" + "vscode-css-languageservice": "^3.0.11-next.2", + "vscode-languageserver": "^5.1.0-next.5" }, "devDependencies": { "@types/mocha": "2.2.33", - "@types/node": "7.0.43" + "@types/node": "^8.10.25", + "glob": "^7.1.2", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts index 8778faf2b79..853f6690473 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 { TextDocument, CompletionList } from 'vscode-languageserver-types'; import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { formatError, runSafe } from './utils/runner'; -import URI from 'vscode-uri'; import { getPathCompletionParticipant } from './pathCompletion'; -import { FoldingRangeServerCapabilities, FoldingRangeRequest } from 'vscode-languageserver-protocol-foldingprovider'; +import { formatError, runSafe } from './utils/runner'; +import { getDocumentContext } from './utils/documentContext'; export interface Settings { css: LanguageSettings; @@ -78,7 +77,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - const capabilities: ServerCapabilities & FoldingRangeServerCapabilities = { + const capabilities: ServerCapabilities = { // Tell the client that the server works in FULL text document sync mode textDocumentSync: documents.syncKind, completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/'] } : undefined, @@ -87,6 +86,9 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { referencesProvider: true, definitionProvider: true, documentHighlightProvider: true, + documentLinkProvider: { + resolveProvider: false + }, codeActionProvider: true, renameProvider: true, colorProvider: {}, @@ -228,29 +230,43 @@ connection.onDocumentSymbol((documentSymbolParams, token) => { }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); }); -connection.onDefinition((documentSymbolParams, token) => { +connection.onDefinition((documentDefinitionParams, token) => { return runSafe(() => { - const document = documents.get(documentSymbolParams.textDocument.uri); + const document = documents.get(documentDefinitionParams.textDocument.uri); if (document) { const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDefinition(document, documentSymbolParams.position, stylesheet); + return getLanguageService(document).findDefinition(document, documentDefinitionParams.position, stylesheet); } return null; - }, null, `Error while computing definitions for ${documentSymbolParams.textDocument.uri}`, token); + }, null, `Error while computing definitions for ${documentDefinitionParams.textDocument.uri}`, token); }); -connection.onDocumentHighlight((documentSymbolParams, token) => { +connection.onDocumentHighlight((documentHighlightParams, token) => { return runSafe(() => { - const document = documents.get(documentSymbolParams.textDocument.uri); + const document = documents.get(documentHighlightParams.textDocument.uri); if (document) { const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDocumentHighlights(document, documentSymbolParams.position, stylesheet); + return getLanguageService(document).findDocumentHighlights(document, documentHighlightParams.position, stylesheet); } return []; - }, [], `Error while computing document highlights for ${documentSymbolParams.textDocument.uri}`, token); + }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); }); + +connection.onDocumentLinks((documentLinkParams, token) => { + return runSafe(() => { + const document = documents.get(documentLinkParams.textDocument.uri); + if (document) { + const documentContext = getDocumentContext(document.uri, workspaceFolders); + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDocumentLinks(document, stylesheet, documentContext); + } + return []; + }, [], `Error while computing document links for ${documentLinkParams.textDocument.uri}`, token); +}); + + connection.onReferences((referenceParams, token) => { return runSafe(() => { const document = documents.get(referenceParams.textDocument.uri); @@ -306,7 +322,7 @@ connection.onRenameRequest((renameParameters, token) => { }, null, `Error while computing renames for ${renameParameters.textDocument.uri}`, token); }); -connection.onRequest(FoldingRangeRequest.type, (params, token) => { +connection.onFoldingRanges((params, token) => { return runSafe(() => { const document = documents.get(params.textDocument.uri); if (document) { diff --git a/extensions/css-language-features/server/src/pathCompletion.ts b/extensions/css-language-features/server/src/pathCompletion.ts index b072c4136f6..ecbd9a09843 100644 --- a/extensions/css-language-features/server/src/pathCompletion.ts +++ b/extensions/css-language-features/server/src/pathCompletion.ts @@ -12,7 +12,7 @@ import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextE import { WorkspaceFolder } from 'vscode-languageserver'; import { ICompletionParticipant } from 'vscode-css-languageservice'; -import { startsWith } from './utils/strings'; +import { startsWith, endsWith } from './utils/strings'; export function getPathCompletionParticipant( document: TextDocument, @@ -21,32 +21,79 @@ export function getPathCompletionParticipant( ): ICompletionParticipant { return { onCssURILiteralValue: ({ position, range, uriValue }) => { - const isValueQuoted = startsWith(uriValue, `'`) || startsWith(uriValue, `"`); const fullValue = stripQuotes(uriValue); - const valueBeforeCursor = isValueQuoted - ? fullValue.slice(0, position.character - (range.start.character + 1)) - : fullValue.slice(0, position.character - range.start.character); - - if (fullValue === '.' || fullValue === '..') { - result.isIncomplete = true; + if (!shouldDoPathCompletion(uriValue, workspaceFolders)) { + if (fullValue === '.' || fullValue === '..') { + result.isIncomplete = true; + } return; } - if (!workspaceFolders || workspaceFolders.length === 0) { + let suggestions = providePathSuggestions(uriValue, position, range, document, workspaceFolders); + result.items = [...suggestions, ...result.items]; + }, + onCssImportPath: ({ position, range, pathValue }) => { + const fullValue = stripQuotes(pathValue); + if (!shouldDoPathCompletion(pathValue, workspaceFolders)) { + if (fullValue === '.' || fullValue === '..') { + result.isIncomplete = true; + } return; } - const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); - const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot); - const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range; - const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange); - const suggestions = paths.map(p => pathToSuggestion(p, replaceRange)); + let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders); + + if (document.languageId === 'scss') { + suggestions.forEach(s => { + if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) { + if (s.textEdit) { + s.textEdit.newText = s.label.slice(1, -5); + } else { + s.label = s.label.slice(1, -5); + } + } + }); + } + result.items = [...suggestions, ...result.items]; } - }; } +function providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, workspaceFolders: WorkspaceFolder[]) { + const fullValue = stripQuotes(pathValue); + const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`); + const valueBeforeCursor = isValueQuoted + ? fullValue.slice(0, position.character - (range.start.character + 1)) + : fullValue.slice(0, position.character - range.start.character); + const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); + const currentDocFsPath = URI.parse(document.uri).fsPath; + + const paths = providePaths(valueBeforeCursor, currentDocFsPath, workspaceRoot).filter(p => { + // Exclude current doc's path + return path.resolve(currentDocFsPath, '../', p) !== currentDocFsPath; + }); + + const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range; + const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange); + + const suggestions = paths.map(p => pathToSuggestion(p, replaceRange)); + return suggestions; +} + +function shouldDoPathCompletion(pathValue: string, workspaceFolders: WorkspaceFolder[]): boolean { + const fullValue = stripQuotes(pathValue); + if (fullValue === '.' || fullValue === '..') { + return false; + } + + if (!workspaceFolders || workspaceFolders.length === 0) { + return false; + } + + return true; +} + function stripQuotes(fullValue: string) { if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { return fullValue.slice(1, -1); @@ -59,16 +106,19 @@ function stripQuotes(fullValue: string) { * Get a list of path suggestions. Folder suggestions are suffixed with a slash. */ function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] { - if (startsWith(valueBeforeCursor, '/') && !root) { - return []; - } - const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1); - const parentDir = startsWith(valueBeforeCursor, '/') - ? path.resolve(root, '.' + valueBeforeLastSlash) - : path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); + const startsWithSlash = startsWith(valueBeforeCursor, '/'); + let parentDir: string; + if (startsWithSlash) { + if (!root) { + return []; + } + parentDir = path.resolve(root, '.' + valueBeforeLastSlash); + } else { + parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); + } try { return fs.readdirSync(parentDir).map(f => { diff --git a/extensions/css-language-features/server/src/test/completion.test.ts b/extensions/css-language-features/server/src/test/completion.test.ts index 62094de6b5b..65cfed74d0c 100644 --- a/extensions/css-language-features/server/src/test/completion.test.ts +++ b/extensions/css-language-features/server/src/test/completion.test.ts @@ -33,11 +33,11 @@ suite('Completions', () => { } }; - function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[]): void { + function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[], lang: string = 'css'): void { const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); - const document = TextDocument.create(testUri, 'css', 0, value); + const document = TextDocument.create(testUri, lang, 0, value); const position = document.positionAt(offset); if (!workspaceFolders) { @@ -61,7 +61,7 @@ suite('Completions', () => { } } - test('CSS Path completion', function () { + test('CSS url() Path completion', function () { let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString(); let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }]; @@ -95,7 +95,6 @@ suite('Completions', () => { assertCompletions(`html { background-image: url('|')`, { items: [ - { label: 'about.css', resultText: `html { background-image: url('about.css')` }, { label: 'about.html', resultText: `html { background-image: url('about.html')` }, ] }, testUri, folders); @@ -121,7 +120,7 @@ suite('Completions', () => { }, testUri, folders); }); - test('CSS Path Completion - Unquoted url', function () { + test('CSS url() Path Completion - Unquoted url', function () { let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString(); let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }]; @@ -149,4 +148,49 @@ suite('Completions', () => { ] }, testUri, folders); }); + + test('CSS @import Path completion', function () { + let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString(); + let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }]; + + assertCompletions(`@import './|'`, { + items: [ + { label: 'about.html', resultText: `@import './about.html'` }, + ] + }, testUri, folders); + + assertCompletions(`@import '../|'`, { + items: [ + { label: 'about/', resultText: `@import '../about/'` }, + { label: 'scss/', resultText: `@import '../scss/'` }, + { label: 'index.html', resultText: `@import '../index.html'` }, + { label: 'src/', resultText: `@import '../src/'` } + ] + }, testUri, folders); + }); + + /** + * For SCSS, `@import 'foo';` can be used for importing partial file `_foo.scss` + */ + test('SCSS @import Path completion', function () { + let testCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).toString(); + let folders = [{ name: 'x', uri: Uri.file(path.resolve(__dirname, '../../test')).toString() }]; + + /** + * We are in a CSS file, so no special treatment for SCSS partial files + */ + assertCompletions(`@import '../scss/|'`, { + items: [ + { label: 'main.scss', resultText: `@import '../scss/main.scss'` }, + { label: '_foo.scss', resultText: `@import '../scss/_foo.scss'` } + ] + }, testCSSUri, folders); + + let testSCSSUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/scss/main.scss')).toString(); + assertCompletions(`@import './|'`, { + items: [ + { label: '_foo.scss', resultText: `@import './foo'` } + ] + }, testSCSSUri, folders, 'scss'); + }); }); \ No newline at end of file diff --git a/extensions/css-language-features/server/src/utils/documentContext.ts b/extensions/css-language-features/server/src/utils/documentContext.ts new file mode 100644 index 00000000000..b37993b7655 --- /dev/null +++ b/extensions/css-language-features/server/src/utils/documentContext.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. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { DocumentContext } from 'vscode-css-languageservice'; +import { endsWith, startsWith } from '../utils/strings'; +import * as url from 'url'; +import { WorkspaceFolder } from 'vscode-languageserver'; + +export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { + function getRootFolder(): string | undefined { + for (let folder of workspaceFolders) { + let folderURI = folder.uri; + if (!endsWith(folderURI, '/')) { + folderURI = folderURI + '/'; + } + if (startsWith(documentUri, folderURI)) { + return folderURI; + } + } + return void 0; + } + + return { + resolveReference: (ref, base = documentUri) => { + if (ref[0] === '/') { // resolve absolute path against the current workspace folder + if (startsWith(base, 'file://')) { + let folderUri = getRootFolder(); + if (folderUri) { + return folderUri + ref.substr(1); + } + } + } + return url.resolve(base, ref); + }, + }; +} + diff --git a/extensions/css-language-features/server/src/utils/strings.ts b/extensions/css-language-features/server/src/utils/strings.ts index f7ad0845cc8..114fb4f0808 100644 --- a/extensions/css-language-features/server/src/utils/strings.ts +++ b/extensions/css-language-features/server/src/utils/strings.ts @@ -17,3 +17,17 @@ export function startsWith(haystack: string, needle: string): boolean { return true; } + +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + let diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.lastIndexOf(needle) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } +} diff --git a/extensions/css-language-features/server/test/index.js b/extensions/css-language-features/server/test/index.js new file mode 100644 index 00000000000..570751f14ce --- /dev/null +++ b/extensions/css-language-features/server/test/index.js @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const Mocha = require('mocha'); +const glob = require('glob'); + +const suite = 'Integration CSS Extension Tests'; + +const options = { + ui: 'tdd', + useColors: true, + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +const mocha = new Mocha(options); + +glob.sync(__dirname + '/../out/test/**/*.test.js') + .forEach(file => mocha.addFile(file)); + +mocha.run(failures => process.exit(failures ? -1 : 0)); diff --git a/extensions/css-language-features/server/test/mocha.opts b/extensions/css-language-features/server/test/mocha.opts deleted file mode 100644 index 97e8b723ae2..00000000000 --- a/extensions/css-language-features/server/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---ui tdd ---useColors true -./out/test \ No newline at end of file diff --git a/extensions/css-language-features/server/test/pathCompletionFixtures/scss/_foo.scss b/extensions/css-language-features/server/test/pathCompletionFixtures/scss/_foo.scss new file mode 100644 index 00000000000..adae63e647c --- /dev/null +++ b/extensions/css-language-features/server/test/pathCompletionFixtures/scss/_foo.scss @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ \ No newline at end of file diff --git a/extensions/css-language-features/server/test/pathCompletionFixtures/scss/main.scss b/extensions/css-language-features/server/test/pathCompletionFixtures/scss/main.scss new file mode 100644 index 00000000000..adae63e647c --- /dev/null +++ b/extensions/css-language-features/server/test/pathCompletionFixtures/scss/main.scss @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ \ No newline at end of file diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index f9b1f5c6468..7e5589b98e8 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -6,50 +6,239 @@ version "2.2.33" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def" -"@types/node@7.0.43": - version "7.0.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" +"@types/node@^8.10.25": + version "8.10.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.25.tgz#801fe4e39372cef18f268db880a5fbfcf71adc7e" -vscode-css-languageservice@^3.0.9-next.18: - version "3.0.9-next.18" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.9-next.18.tgz#f8f25123b5a8cdc9f72fafcd2c0088f726322437" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: - vscode-languageserver-types "^3.7.2" - vscode-nls "^3.2.2" + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + +debug@3.1.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@7.1.2, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +lodash@^4.16.4: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha-junit-reporter@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c" + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +vscode-css-languageservice@^3.0.11-next.2: + version "3.0.11-next.2" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.11-next.2.tgz#7aa41a895abbcb3c0884d9612ae546c39de8b984" + dependencies: + vscode-languageserver-types "^3.12.0" + vscode-nls "^3.2.5" vscode-jsonrpc@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8" -vscode-languageserver-protocol-foldingprovider@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol-foldingprovider/-/vscode-languageserver-protocol-foldingprovider-2.0.1.tgz#051d0d9e58d1b79dc4681acd48f21797f5515bfd" - dependencies: - vscode-languageserver-protocol "^3.7.2" - vscode-languageserver-types "^3.7.2" - -vscode-languageserver-protocol@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.7.2.tgz#df58621c032139010888b6a9ddc969423f9ba9d6" +vscode-languageserver-protocol@^3.13.0-next.1: + version "3.13.0-next.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.13.0-next.1.tgz#24d15aa5769e4b035f6ce38342685959e1eb454e" dependencies: vscode-jsonrpc "^3.6.2" - vscode-languageserver-types "^3.7.2" + vscode-languageserver-types "^3.13.0-next.1" -vscode-languageserver-types@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.7.2.tgz#aad8846f8e3e27962648554de5a8417e358f34eb" +vscode-languageserver-types@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.12.0.tgz#f96051381b6a050b7175b37d6cb5d2f2eb64b944" -vscode-languageserver@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-4.1.3.tgz#937d37c955b6b9c2409388413cd6f54d1eb9fe7d" +vscode-languageserver-types@^3.13.0-next.1: + version "3.13.0-next.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.13.0-next.1.tgz#dd91b78b13a2a6c7131e8291e3db3183cf566280" + +vscode-languageserver@^5.1.0-next.5: + version "5.1.0-next.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-5.1.0-next.5.tgz#d343278b4de448a31f9db146b98638898c8d4f55" dependencies: - vscode-languageserver-protocol "^3.7.2" - vscode-uri "^1.0.1" + vscode-languageserver-protocol "^3.13.0-next.1" + vscode-uri "^1.0.5" -vscode-nls@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +vscode-nls@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" -vscode-uri@^1.0.1: +vscode-uri@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xml@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.1.tgz#11a86befeac3c4aa3ec08623651a3c81a6d0bbc8" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" diff --git a/extensions/css-language-features/test/mocha.opts b/extensions/css-language-features/test/mocha.opts new file mode 100644 index 00000000000..20fcfb6eef6 --- /dev/null +++ b/extensions/css-language-features/test/mocha.opts @@ -0,0 +1,3 @@ +--ui tdd +--useColors true +server/out/test/**.test.js \ No newline at end of file diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 98f6af48a34..b0af5d6319b 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,38 +2,167 @@ # yarn lockfile v1 -"@types/node@7.0.43": - version "7.0.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" +"@types/node@^8.10.25": + version "8.10.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.25.tgz#801fe4e39372cef18f268db880a5fbfcf71adc7e" -vscode-jsonrpc@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" -vscode-languageclient@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-4.1.4.tgz#fff1a6bca4714835dca7fce35bc4ce81442fdf2c" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: - vscode-languageserver-protocol "^3.7.2" + balanced-match "^1.0.0" + concat-map "0.0.1" -vscode-languageserver-protocol-foldingprovider@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol-foldingprovider/-/vscode-languageserver-protocol-foldingprovider-2.0.1.tgz#051d0d9e58d1b79dc4681acd48f21797f5515bfd" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: - vscode-languageserver-protocol "^3.7.2" - vscode-languageserver-types "^3.7.2" + ms "2.0.0" -vscode-languageserver-protocol@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.7.2.tgz#df58621c032139010888b6a9ddc969423f9ba9d6" +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: - vscode-jsonrpc "^3.6.2" - vscode-languageserver-types "^3.7.2" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" -vscode-languageserver-types@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.7.2.tgz#aad8846f8e3e27962648554de5a8417e358f34eb" +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" -vscode-nls@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +semver@^5.5.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +vscode-jsonrpc@^3.7.0-next.1: + version "3.7.0-next.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.7.0-next.1.tgz#e7521cd8135006ba8bf57ebcedbf5b03ce17b23e" + +vscode-languageclient@^5.1.0-next.9: + version "5.1.0-next.9" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.1.0-next.9.tgz#fe56c523637c118f7b262952b889f760223ab75a" + dependencies: + semver "^5.5.0" + vscode-languageserver-protocol "^3.13.0-next.2" + +vscode-languageserver-protocol@^3.13.0-next.2: + version "3.13.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.13.0-next.2.tgz#7083e50d0d2096ba52de448082445b9c39081fdb" + dependencies: + vscode-jsonrpc "^3.7.0-next.1" + vscode-languageserver-types "^3.13.0-next.1" + +vscode-languageserver-types@^3.13.0-next.1: + version "3.13.0-next.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.13.0-next.1.tgz#dd91b78b13a2a6c7131e8291e3db3183cf566280" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" diff --git a/extensions/css/.vscode/launch.json b/extensions/css/.vscode/launch.json new file mode 100644 index 00000000000..2217bbd0770 --- /dev/null +++ b/extensions/css/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Grammar", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}" + ] + } + ] +} diff --git a/extensions/css/package.json b/extensions/css/package.json index 9517029a69f..13f737e92fc 100644 --- a/extensions/css/package.json +++ b/extensions/css/package.json @@ -8,7 +8,7 @@ "vscode": "0.10.x" }, "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js atom/language-css grammars/css.cson ./syntaxes/css.tmLanguage.json" + "update-grammar": "node ../../build/npm/update-grammar.js octref/language-css grammars/css.cson ./syntaxes/css.tmLanguage.json" }, "contributes": { "languages": [ diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index a8fc2fe565e..e0721094a00 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/atom/language-css/blob/master/grammars/css.cson", + "This file has been converted from https://github.com/octref/language-css/blob/master/grammars/css.cson", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-css/commit/2bc1e294e2440ad91197263cd9f95dc4b00bab2f", + "version": "https://github.com/octref/language-css/commit/c2c3933bbd62f867acf68042f9e82fa1041c4101", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -508,7 +508,7 @@ ] }, { - "begin": "(?i)((@)viewport)(?=[\\s'\"{;]|/\\*|$)", + "begin": "(?i)((@)(-ms-|-o-)?viewport)(?=[\\s'\"{;]|/\\*|$)", "beginCaptures": { "1": { "name": "keyword.control.at-rule.viewport.css" @@ -604,6 +604,45 @@ "include": "#string" } ] + }, + { + "begin": "(?i)(?=@[\\w-]+(\\s|\\(|{|;|/\\*|$))", + "end": "(?<=}|;)(?!\\G)", + "patterns": [ + { + "begin": "(?i)\\G(@)[\\w-]+", + "beginCaptures": { + "0": { + "name": "keyword.control.at-rule.css" + }, + "1": { + "name": "punctuation.definition.keyword.css" + } + }, + "end": "(?=\\s*[{;])", + "name": "meta.at-rule.header.css" + }, + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.section.begin.bracket.curly.css" + } + }, + "end": "}", + "endCaptures": { + "0": { + "name": "punctuation.section.end.bracket.curly.css" + } + }, + "name": "meta.at-rule.body.css", + "patterns": [ + { + "include": "$self" + } + ] + } + ] } ] }, @@ -1338,7 +1377,7 @@ "property-keywords": { "patterns": [ { - "match": "(?xi) (? { + if (e.affectsConfiguration(DEBUG_SETTINGS + '.' + AUTO_ATTACH_SETTING)) { + updateAutoAttach(context); + } + })); + + updateAutoAttach(context); +} + +export function deactivate(): void { +} + + +function toggleAutoAttachSetting(context: vscode.ExtensionContext) { + + const conf = vscode.workspace.getConfiguration(DEBUG_SETTINGS); + if (conf) { + let value = conf.get(AUTO_ATTACH_SETTING); + if (value === 'on') { + value = 'off'; + } else { + value = 'on'; + } + + const info = conf.inspect(AUTO_ATTACH_SETTING); + let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global; + if (info) { + if (info.workspaceFolderValue) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + } else if (info.workspaceValue) { + target = vscode.ConfigurationTarget.Workspace; + } else if (info.globalValue) { + target = vscode.ConfigurationTarget.Global; + } else if (info.defaultValue) { + // setting not yet used: store setting in workspace + if (vscode.workspace.workspaceFolders) { + target = vscode.ConfigurationTarget.Workspace; + } + } + } + conf.update(AUTO_ATTACH_SETTING, value, target); + } +} + +/** + * Updates the auto attach feature based on the user or workspace setting + */ +function updateAutoAttach(context: vscode.ExtensionContext) { + + const newState = vscode.workspace.getConfiguration(DEBUG_SETTINGS).get(AUTO_ATTACH_SETTING); + + if (newState !== currentState) { + + if (newState === 'disabled') { + + // turn everything off + if (statusItem) { + statusItem.hide(); + statusItem.text = OFF_TEXT; + } + if (autoAttachStarted) { + vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => { + currentState = newState; + autoAttachStarted = false; + }); + } + + } else { // 'on' or 'off' + + // make sure status bar item exists and is visible + if (!statusItem) { + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + statusItem.command = TOGGLE_COMMAND; + statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode"); + statusItem.show(); + context.subscriptions.push(statusItem); + } else { + statusItem.show(); + } + + if (newState === 'off') { + if (autoAttachStarted) { + vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => { + currentState = newState; + if (statusItem) { + statusItem.text = OFF_TEXT; + } + autoAttachStarted = false; + }); + } + + } else if (newState === 'on') { + + const vscode_pid = process.env['VSCODE_PID']; + const rootPid = vscode_pid ? parseInt(vscode_pid) : 0; + vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid).then(_ => { + if (statusItem) { + statusItem.text = ON_TEXT; + } + currentState = newState; + autoAttachStarted = true; + }); + } + } + } +} diff --git a/extensions/debug-auto-launch/src/typings/ref.d.ts b/extensions/debug-auto-launch/src/typings/ref.d.ts new file mode 100644 index 00000000000..bc057c55878 --- /dev/null +++ b/extensions/debug-auto-launch/src/typings/ref.d.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * 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/debug-auto-launch/tsconfig.json b/extensions/debug-auto-launch/tsconfig.json new file mode 100644 index 00000000000..2a5517b5543 --- /dev/null +++ b/extensions/debug-auto-launch/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "outDir": "./out", + "lib": [ + "es2015" + ], + "strict": true, + "noUnusedLocals": true, + "downlevelIteration": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock new file mode 100644 index 00000000000..888e8da826d --- /dev/null +++ b/extensions/debug-auto-launch/yarn.lock @@ -0,0 +1,11 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@8.0.33": + version "8.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" diff --git a/extensions/docker/test/colorize-results/Dockerfile.json b/extensions/docker/test/colorize-results/Dockerfile.json index a18ec445c04..fcb2e004c16 100644 --- a/extensions/docker/test/colorize-results/Dockerfile.json +++ b/extensions/docker/test/colorize-results/Dockerfile.json @@ -179,9 +179,9 @@ "c": "#", "t": "source.dockerfile comment.line.number-sign.dockerfile punctuation.definition.comment.dockerfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } @@ -190,9 +190,9 @@ "c": "RUN apt-get install -y nodejs=0.6.12~dfsg1-1ubuntu1", "t": "source.dockerfile comment.line.number-sign.dockerfile", "r": { - "dark_plus": "comment: #608B4E", + "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", - "dark_vs": "comment: #608B4E", + "dark_vs": "comment: #6A9955", "light_vs": "comment: #008000", "hc_black": "comment: #7CA668" } diff --git a/extensions/emmet/.vscodeignore b/extensions/emmet/.vscodeignore index ebab1d50b9b..729e3f56400 100644 --- a/extensions/emmet/.vscodeignore +++ b/extensions/emmet/.vscodeignore @@ -1,3 +1,6 @@ test/** src/** -tsconfig.json \ No newline at end of file +out/** +tsconfig.json +extension.webpack.config.js + diff --git a/extensions/emmet/CONTRIBUTING.md b/extensions/emmet/CONTRIBUTING.md new file mode 100644 index 00000000000..c6c334828c3 --- /dev/null +++ b/extensions/emmet/CONTRIBUTING.md @@ -0,0 +1,14 @@ +## How to build and run from source? + +Read the basics about extension authoring from [Extending Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) + +- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code +- Open the `extensions/emmet` folder in the vscode repo in VS Code +- Press F5 to start debugging + +## Running tests + +Tests for Emmet extension are run as integration tests as part of VS Code. + +- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code +- Run `./scripts/test-integration.sh` to run all the integrations tests that include the Emmet tests. \ No newline at end of file diff --git a/extensions/emmet/README.md b/extensions/emmet/README.md index a2720bbd541..b755345f787 100644 --- a/extensions/emmet/README.md +++ b/extensions/emmet/README.md @@ -1,25 +1,9 @@ # Emmet integration in Visual Studio Code +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + ## Features -See [Emmet in Visual Studio Code](https://code.visualstudio.com/docs/editor/emmet) - -## How to build and run from source? - -Read the basics about extension authoring from [Extending Visual Studio Code](https://code.visualstudio.com/docs/extensions/overview) - -- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code -- Open the `extensions/emmet` folder in the vscode repo in VS Code -- Press F5 to start debugging - -## Running tests - -Tests for Emmet extension are run as integration tests as part of VS Code. - -- Read [Build and Run VS Code from Source](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source) to get a local dev set up running for VS Code -- Run `./scripts/test-integration.sh` to run all the integrations tests that include the Emmet tests. - - - - +See [Emmet in Visual Studio Code](https://code.visualstudio.com/docs/editor/emmet) to learn about the features of this extension. +Please read the [CONTRIBUTING.md](https://github.com/Microsoft/vscode/blob/master/extensions/emmet/CONTRIBUTING.md) file to learn how to contribute to this extension. \ No newline at end of file diff --git a/extensions/emmet/extension.webpack.config.js b/extensions/emmet/extension.webpack.config.js new file mode 100644 index 00000000000..d5064ef4517 --- /dev/null +++ b/extensions/emmet/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + externals: { + 'vscode-emmet-helper': 'commonjs vscode-emmet-helper', + }, +}); diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index a8d65da831f..d9be62f74d1 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -39,17 +39,17 @@ "inMarkupAndStylesheetFilesOnly" ], "default": "always", - "description": "%emmetShowExpandedAbbreviation%" + "markdownDescription": "%emmetShowExpandedAbbreviation%" }, "emmet.showAbbreviationSuggestions": { "type": "boolean", "default": true, - "description": "%emmetShowAbbreviationSuggestions%" + "markdownDescription": "%emmetShowAbbreviationSuggestions%" }, "emmet.includeLanguages": { "type": "object", "default": {}, - "description": "%emmetIncludeLanguages%" + "markdownDescription": "%emmetIncludeLanguages%" }, "emmet.variables": { "type": "object", @@ -183,22 +183,22 @@ "css.webkitProperties": { "type": "string", "default": null, - "description": "%emmetPreferencesCssWebkitProperties%" + "markdownDescription": "%emmetPreferencesCssWebkitProperties%" }, "css.mozProperties": { "type": "string", "default": null, - "description": "%emmetPreferencesCssMozProperties%" + "markdownDescription": "%emmetPreferencesCssMozProperties%" }, "css.oProperties": { "type": "string", "default": null, - "description": "%emmetPreferencesCssOProperties%" + "markdownDescription": "%emmetPreferencesCssOProperties%" }, "css.msProperties": { "type": "string", "default": null, - "description": "%emmetPreferencesCssMsProperties%" + "markdownDescription": "%emmetPreferencesCssMsProperties%" }, "css.fuzzySearchMinScore": { "type": "number", @@ -210,12 +210,12 @@ "emmet.showSuggestionsAsSnippets": { "type": "boolean", "default": false, - "description": "%emmetShowSuggestionsAsSnippets%" + "markdownDescription": "%emmetShowSuggestionsAsSnippets%" }, "emmet.optimizeStylesheetParsing": { "type": "boolean", "default": true, - "description": "%emmetOptimizeStylesheetParsing%" + "markdownDescription": "%emmetOptimizeStylesheetParsing%" } } }, @@ -438,15 +438,15 @@ }, "devDependencies": { "@types/node": "8.0.33", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", "vscode": "1.0.1" }, "dependencies": { - "@emmetio/html-matcher": "^0.3.3", "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", + "@emmetio/html-matcher": "^0.3.3", "@emmetio/math-expression": "^0.1.1", - "vscode-emmet-helper": "^1.2.9", - "vscode-languageserver-types": "^3.5.0", "image-size": "^0.5.2", - "vscode-nls": "3.2.1" + "vscode-emmet-helper": "^1.2.11" } -} \ No newline at end of file +} diff --git a/extensions/emmet/package.nls.json b/extensions/emmet/package.nls.json index d43ba09bc46..3f3bf912fd8 100644 --- a/extensions/emmet/package.nls.json +++ b/extensions/emmet/package.nls.json @@ -25,10 +25,10 @@ "command.decrementNumberByTen": "Decrement by 10", "emmetSyntaxProfiles": "Define profile for specified syntax or use your own profile with specific rules.", "emmetExclude": "An array of languages where Emmet abbreviations should not be expanded.", - "emmetExtensionsPath": "Path to a folder containing Emmet profiles and snippets.'", - "emmetShowExpandedAbbreviation": "Shows expanded Emmet abbreviations as suggestions.\nThe option \"inMarkupAndStylesheetFilesOnly\" applies to html, haml, jade, slim, xml, xsl, css, scss, sass, less and stylus.\nThe option \"always\" applies to all parts of the file regardless of markup/css.", - "emmetShowAbbreviationSuggestions": "Shows possible Emmet abbreviations as suggestions. Not applicable in stylesheets or when emmet.showExpandedAbbreviation is set to \"never\".", - "emmetIncludeLanguages": "Enable Emmet abbreviations in languages that are not supported by default. Add a mapping here between the language and emmet supported language.\n E.g.: {\"vue-html\": \"html\", \"javascript\": \"javascriptreact\"}", + "emmetExtensionsPath": "Path to a folder containing Emmet profiles and snippets.", + "emmetShowExpandedAbbreviation": "Shows expanded Emmet abbreviations as suggestions.\nThe option `\"inMarkupAndStylesheetFilesOnly\"` applies to html, haml, jade, slim, xml, xsl, css, scss, sass, less and stylus.\nThe option `\"always\"` applies to all parts of the file regardless of markup/css.", + "emmetShowAbbreviationSuggestions": "Shows possible Emmet abbreviations as suggestions. Not applicable in stylesheets or when emmet.showExpandedAbbreviation is set to `\"never\"`.", + "emmetIncludeLanguages": "Enable Emmet abbreviations in languages that are not supported by default. Add a mapping here between the language and emmet supported language.\n E.g.: `{\"vue-html\": \"html\", \"javascript\": \"javascriptreact\"}`", "emmetVariables": "Variables to be used in Emmet snippets", "emmetTriggerExpansionOnTab": "When enabled, Emmet abbreviations are expanded when pressing TAB.", "emmetPreferences": "Preferences used to modify behavior of some actions and resolvers of Emmet.", @@ -40,7 +40,7 @@ "emmetPreferencesCssBetween": "Symbol to be placed at the between CSS property and value when expanding CSS abbreviations", "emmetPreferencesSassBetween": "Symbol to be placed at the between CSS property and value when expanding CSS abbreviations in Sass files", "emmetPreferencesStylusBetween": "Symbol to be placed at the between CSS property and value when expanding CSS abbreviations in Stylus files", - "emmetShowSuggestionsAsSnippets": "If true, then Emmet suggestions will show up as snippets allowing you to order them as per editor.snippetSuggestions setting.", + "emmetShowSuggestionsAsSnippets": "If `true`, then Emmet suggestions will show up as snippets allowing you to order them as per `#editor.snippetSuggestions#` setting.", "emmetPreferencesBemElementSeparator": "Element separator used for classes when using the BEM filter", "emmetPreferencesBemModifierSeparator": "Modifier separator used for classes when using the BEM filter", "emmetPreferencesFilterCommentBefore": "A definition of comment that should be placed before matched element when comment filter is applied.", @@ -54,5 +54,5 @@ "emmetPreferencesCssOProperties": "Comma separated CSS properties that get the 'o' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'o' prefix.", "emmetPreferencesCssMsProperties": "Comma separated CSS properties that get the 'ms' vendor prefix when used in Emmet abbreviation that starts with `-`. Set to empty string to always avoid the 'ms' prefix.", "emmetPreferencesCssFuzzySearchMinScore": "The minimum score (from 0 to 1) that fuzzy-matched abbreviation should achieve. Lower values may produce many false-positive matches, higher values may reduce possible matches.", - "emmetOptimizeStylesheetParsing": "When set to false, the whole file is parsed to determine if current position is valid for expanding Emmet abbreviations. When set to true, only the content around the current position in css/scss/less files is parsed." + "emmetOptimizeStylesheetParsing": "When set to `false`, the whole file is parsed to determine if current position is valid for expanding Emmet abbreviations. When set to `true`, only the content around the current position in css/scss/less files is parsed." } diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 55f1c1ac986..26444af31e5 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -96,6 +96,7 @@ function doWrapping(individualLines: boolean, args: any) { const preceedingWhiteSpace = otherMatches ? otherMatches[1] : ''; textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + preceedingWhiteSpace).join('\n\t') + '\n']; } + textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1')); return { previewRange: rangeToReplace, @@ -497,6 +498,12 @@ export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocumen i--; continue; } + // Fix for https://github.com/Microsoft/vscode/issues/55411 + // A space is not a valid character right after < in a tag name. + if (/\s/.test(char) && textToBackTrack[i] === startAngle) { + i--; + continue; + } if (char !== startAngle && char !== endAngle) { continue; } diff --git a/extensions/emmet/src/balance.ts b/extensions/emmet/src/balance.ts index 5e323f29e22..4687b734524 100644 --- a/extensions/emmet/src/balance.ts +++ b/extensions/emmet/src/balance.ts @@ -61,7 +61,7 @@ function balance(out: boolean) { } function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection { - let nodeToBalance = getHtmlNode(document, rootNode, selection.start); + let nodeToBalance = getHtmlNode(document, rootNode, selection.start, false); if (!nodeToBalance) { return selection; } diff --git a/extensions/emmet/src/locateFile.ts b/extensions/emmet/src/locateFile.ts index f3fd2146a11..0ef2ad697ca 100644 --- a/extensions/emmet/src/locateFile.ts +++ b/extensions/emmet/src/locateFile.ts @@ -10,7 +10,9 @@ import * as path from 'path'; import * as fs from 'fs'; -const reAbsolute = /^\/+/; +const reAbsolutePosix = /^\/+/; +const reAbsoluteWin32 = /^\\+/; +const reAbsolute = path.sep === '/' ? reAbsolutePosix : reAbsoluteWin32; /** * Locates given `filePath` on user’s file system and returns absolute path to it. diff --git a/extensions/emmet/src/mergeLines.ts b/extensions/emmet/src/mergeLines.ts index 848966de4d1..cebcea3010f 100644 --- a/extensions/emmet/src/mergeLines.ts +++ b/extensions/emmet/src/mergeLines.ts @@ -34,7 +34,7 @@ function getRangesToReplace(document: vscode.TextDocument, selection: vscode.Sel let endNodeToUpdate: Node | null; if (selection.isEmpty) { - startNodeToUpdate = endNodeToUpdate = getNode(rootNode, selection.start); + startNodeToUpdate = endNodeToUpdate = getNode(rootNode, selection.start, true); } else { startNodeToUpdate = getNode(rootNode, selection.start, true); endNodeToUpdate = getNode(rootNode, selection.end, true); diff --git a/extensions/emmet/src/removeTag.ts b/extensions/emmet/src/removeTag.ts index 0ea6e4dde96..9209a6d75e8 100644 --- a/extensions/emmet/src/removeTag.ts +++ b/extensions/emmet/src/removeTag.ts @@ -38,7 +38,7 @@ export function removeTag() { function getRangeToRemove(editor: vscode.TextEditor, rootNode: HtmlNode, selection: vscode.Selection, indentInSpaces: string): vscode.Range[] { - let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start); + let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true); if (!nodeToUpdate) { return []; } diff --git a/extensions/emmet/src/selectItemHTML.ts b/extensions/emmet/src/selectItemHTML.ts index d5dbbf33d05..6fe1825fd87 100644 --- a/extensions/emmet/src/selectItemHTML.ts +++ b/extensions/emmet/src/selectItemHTML.ts @@ -8,7 +8,7 @@ import { getDeepestNode, findNextWord, findPrevWord, getHtmlNode } from './util' import { HtmlNode } from 'EmmetNode'; export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined { - let currentNode = getHtmlNode(editor.document, rootNode, selectionEnd); + let currentNode = getHtmlNode(editor.document, rootNode, selectionEnd, false); let nextNode: HtmlNode | undefined = undefined; if (!currentNode) { @@ -31,7 +31,7 @@ export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vsco // Get the first child of current node which is right after the cursor and is not a comment nextNode = currentNode.firstChild; - while (nextNode && (selectionEnd.isAfterOrEqual(nextNode.start) || nextNode.type === 'comment')) { + while (nextNode && (selectionEnd.isAfterOrEqual(nextNode.end) || nextNode.type === 'comment')) { nextNode = nextNode.nextSibling; } } @@ -54,7 +54,7 @@ export function nextItemHTML(selectionStart: vscode.Position, selectionEnd: vsco } export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vscode.Position, editor: vscode.TextEditor, rootNode: HtmlNode): vscode.Selection | undefined { - let currentNode = getHtmlNode(editor.document, rootNode, selectionStart); + let currentNode = getHtmlNode(editor.document, rootNode, selectionStart, false); let prevNode: HtmlNode | undefined = undefined; if (!currentNode) { @@ -63,7 +63,7 @@ export function prevItemHTML(selectionStart: vscode.Position, selectionEnd: vsco if (currentNode.type !== 'comment' && selectionStart.translate(0, -1).isAfter(currentNode.open.start)) { - if (selectionStart.isBefore(currentNode.open.end) || !currentNode.firstChild) { + if (selectionStart.isBefore(currentNode.open.end) || !currentNode.firstChild || selectionEnd.isBeforeOrEqual(currentNode.firstChild.start)) { prevNode = currentNode; } else { // Select the child that appears just before the cursor and is not a comment diff --git a/extensions/emmet/src/selectItemStylesheet.ts b/extensions/emmet/src/selectItemStylesheet.ts index 3a63a8cb79f..29df659c564 100644 --- a/extensions/emmet/src/selectItemStylesheet.ts +++ b/extensions/emmet/src/selectItemStylesheet.ts @@ -51,7 +51,7 @@ export function nextItemStylesheet(startOffset: vscode.Position, endOffset: vsco } export function prevItemStylesheet(startOffset: vscode.Position, endOffset: vscode.Position, editor: vscode.TextEditor, rootNode: CssNode): vscode.Selection | undefined { - let currentNode = getNode(rootNode, startOffset); + let currentNode = getNode(rootNode, startOffset, false); if (!currentNode) { currentNode = rootNode; } diff --git a/extensions/emmet/src/splitJoinTag.ts b/extensions/emmet/src/splitJoinTag.ts index c437d5d14f3..a5f1d255c85 100644 --- a/extensions/emmet/src/splitJoinTag.ts +++ b/extensions/emmet/src/splitJoinTag.ts @@ -20,7 +20,7 @@ export function splitJoinTag() { return editor.edit(editBuilder => { editor.selections.reverse().forEach(selection => { - let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start); + let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true); if (nodeToUpdate) { let textEdit = getRangesToReplace(editor.document, nodeToUpdate); editBuilder.replace(textEdit.range, textEdit.newText); diff --git a/extensions/emmet/src/test/abbreviationAction.test.ts b/extensions/emmet/src/test/abbreviationAction.test.ts index 14286654d05..b5fb87d8b07 100644 --- a/extensions/emmet/src/test/abbreviationAction.test.ts +++ b/extensions/emmet/src/test/abbreviationAction.test.ts @@ -466,6 +466,16 @@ suite('Tests for jsx, xml and xsl', () => { }); }); + test('Expand abbreviation with condition containing less than sign for jsx', () => { + return withRandomFileEditor('if (foo < 10) { span.bar', 'javascriptreact', (editor, doc) => { + editor.selection = new Selection(0, 27, 0, 27); + return expandEmmetAbbreviation({ language: 'javascriptreact' }).then(() => { + assert.equal(editor.document.getText(), 'if (foo < 10) { '); + return Promise.resolve(); + }); + }); + }); + test('No expanding text inside open tag in completion list (jsx)', () => { return testNoCompletion('jsx', htmlContents, new Selection(2, 4, 2, 4)); }); diff --git a/extensions/emmet/src/test/editPointSelectItemBalance.test.ts b/extensions/emmet/src/test/editPointSelectItemBalance.test.ts index a9f25685b91..f05469ab440 100644 --- a/extensions/emmet/src/test/editPointSelectItemBalance.test.ts +++ b/extensions/emmet/src/test/editPointSelectItemBalance.test.ts @@ -113,6 +113,22 @@ suite('Tests for Next/Previous Select/Edit point and Balance actions', () => { }); }); + test('Emmet Select Next/Prev item at boundary', function(): any { + return withRandomFileEditor(htmlContents, '.html', (editor, doc) => { + editor.selections = [new Selection(4, 1, 4, 1)]; + + fetchSelectItem('next'); + testSelection(editor.selection, 2, 4, 6); + + editor.selections = [new Selection(4, 1, 4, 1)]; + + fetchSelectItem('prev'); + testSelection(editor.selection, 1, 3, 5); + + return Promise.resolve(); + }); + }); + test('Emmet Next/Prev Item in html template', function (): any { const templateContents = ` + \ No newline at end of file diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js new file mode 100644 index 00000000000..daf601e7425 --- /dev/null +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +const perf = require('../../../base/common/performance'); +perf.mark('renderer/started'); + +const bootstrapWindow = require('../../../../bootstrap-window'); + +// Setup shell environment +process['lazyEnv'] = getLazyEnv(); + +// Load workbench main +bootstrapWindow.load([ + 'vs/workbench/workbench.main', + 'vs/nls!vs/workbench/workbench.main', + 'vs/css!vs/workbench/workbench.main' +], + function (workbench, configuration) { + perf.mark('didLoadWorkbenchMain'); + + return process['lazyEnv'].then(function () { + perf.mark('main/startup'); + + // @ts-ignore + return require('vs/workbench/electron-browser/main').startup(configuration); + }); + }, { + removeDeveloperKeybindingsAfterLoad: true, + canModifyDOM: function (windowConfig) { + showPartsSplash(windowConfig); + }, + beforeLoaderConfig: function (windowConfig, loaderConfig) { + const onNodeCachedData = window['MonacoEnvironment'].onNodeCachedData = []; + loaderConfig.onNodeCachedData = function () { + onNodeCachedData.push(arguments); + }; + + loaderConfig.recordStats = !!windowConfig.performance; + }, + beforeRequire: function () { + perf.mark('willLoadWorkbenchMain'); + } + }); + +/** + * @param {object} configuration + */ +function showPartsSplash(configuration) { + perf.mark('willShowPartsSplash'); + + // TODO@Ben remove me after a while + perf.mark('willAccessLocalStorage'); + let storage = window.localStorage; + perf.mark('didAccessLocalStorage'); + + let data; + try { + let raw = storage.getItem('storage://global/parts-splash-data'); + data = JSON.parse(raw); + } catch (e) { + // ignore + } + + // high contrast mode has been turned on, ignore stored colors and layouts + if (data && configuration.highContrast && data.baseTheme !== 'hc-black') { + data = void 0; + } + + const style = document.createElement('style'); + document.head.appendChild(style); + + if (data) { + const { layoutInfo, colorInfo, baseTheme } = data; + + // set the theme base id used by images and some styles + document.body.className = `monaco-shell ${baseTheme}`; + // stylesheet that defines foreground and background color + style.innerHTML = `.monaco-shell { background-color: ${colorInfo.editorBackground}; color: ${colorInfo.foreground}; }`; + + const splash = document.createElement('div'); + splash.id = data.id; + + // ensure there is enough space + layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth + layoutInfo.editorPartMinWidth)); + + if (configuration.folderUri || configuration.workspace) { + // folder or workspace -> status bar color, sidebar + splash.innerHTML = ` +
+
+
+
+ `; + } else { + // empty -> speical status bar color, no sidebar + splash.innerHTML = ` +
+
+
+ `; + } + document.body.appendChild(splash); + } else { + document.body.className = `monaco-shell ${configuration.highContrast ? 'hc-black' : 'vs-dark'}`; + style.innerHTML = `.monaco-shell { background-color: ${configuration.highContrast ? '#000000' : '#1E1E1E'}; color: ${configuration.highContrast ? '#FFFFFF' : '#CCCCCC'}; }`; + } + + perf.mark('didShowPartsSplash'); +} + +/** + * @returns {Promise} + */ +function getLazyEnv() { + // @ts-ignore + const ipc = require('electron').ipcRenderer; + + return new Promise(function (resolve) { + const handle = setTimeout(function () { + resolve(); + console.warn('renderer did not receive lazyEnv in time'); + }, 10000); + + ipc.once('vscode:acceptShellEnv', function (event, shellEnv) { + clearTimeout(handle); + bootstrapWindow.assign(process.env, shellEnv); + resolve(process.env); + }); + + ipc.send('vscode:fetchShellEnv'); + }); +} \ No newline at end of file diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ea93934523a..7c8dc75333e 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -5,22 +5,21 @@ 'use strict'; -import { app, ipcMain as ipc } from 'electron'; +import { app, ipcMain as ipc, systemPreferences, shell, Event } from 'electron'; import * as platform from 'vs/base/common/platform'; import { WindowsManager } from 'vs/code/electron-main/windows'; import { IWindowsService, OpenContext, ActiveWindowManager } from 'vs/platform/windows/common/windows'; -import { WindowsChannel } from 'vs/platform/windows/common/windowsIpc'; +import { WindowsChannel } from 'vs/platform/windows/node/windowsIpc'; import { WindowsService } from 'vs/platform/windows/electron-main/windowsService'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { CodeMenu } from 'vs/code/electron-main/menus'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; -import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; +import { UpdateChannel } from 'vs/platform/update/node/updateIpc'; import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { Mutex } from 'windows-mutex'; -import { LaunchService, LaunchChannel, ILaunchService } from './launch'; +import { LaunchService, LaunchChannel, ILaunchService } from 'vs/platform/launch/electron-main/launchService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -29,38 +28,47 @@ import { IStateService } from 'vs/platform/state/common/state'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IURLService } from 'vs/platform/url/common/url'; -import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/common/urlIpc'; +import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/node/urlIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; +import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc'; import product from 'vs/platform/node/product'; import pkg from 'vs/platform/node/package'; -import { ProxyAuthHandler } from './auth'; +import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { CodeWindow } from 'vs/code/electron-main/window'; import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard'; -import URI from 'vs/base/common/uri'; -import { WorkspacesChannel } from 'vs/platform/workspaces/common/workspacesIpc'; +import { URI } from 'vs/base/common/uri'; +import { WorkspacesChannel } from 'vs/platform/workspaces/node/workspacesIpc'; import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; import { IIssueService } from 'vs/platform/issue/common/issue'; -import { IssueChannel } from 'vs/platform/issue/common/issueIpc'; +import { IssueChannel } from 'vs/platform/issue/node/issueIpc'; import { IssueService } from 'vs/platform/issue/electron-main/issueService'; -import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; -import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc'; +import * as errors from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; +import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService'; +import { MenubarChannel } from 'vs/platform/menubar/node/menubarIpc'; +import { ILabelService, RegisterFormatterEvent } from 'vs/platform/label/common/label'; +import { CodeMenu } from 'vs/code/electron-main/menus'; +import { hasArgs } from 'vs/platform/environment/node/argv'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu'; +import { THEME_STORAGE_KEY, THEME_BG_STORAGE_KEY } from 'vs/code/electron-main/theme'; +import { nativeSep } from 'vs/base/common/paths'; export class CodeApplication { @@ -81,9 +89,10 @@ export class CodeApplication { @ILogService private logService: ILogService, @IEnvironmentService private environmentService: IEnvironmentService, @ILifecycleService private lifecycleService: ILifecycleService, - @IConfigurationService configurationService: ConfigurationService, + @IConfigurationService private configurationService: ConfigurationService, @IStateService private stateService: IStateService, - @IHistoryMainService private historyMainService: IHistoryMainService + @IHistoryMainService private historyMainService: IHistoryMainService, + @ILabelService private labelService: ILabelService ) { this.toDispose = [mainIpcServer, configurationService]; @@ -93,8 +102,12 @@ export class CodeApplication { private registerListeners(): void { // We handle uncaught exceptions here to prevent electron from opening a dialog to the user - setUnexpectedErrorHandler(err => this.onUnexpectedError(err)); + errors.setUnexpectedErrorHandler(err => this.onUnexpectedError(err)); process.on('uncaughtException', err => this.onUnexpectedError(err)); + process.on('unhandledRejection', (reason: any, promise: Promise) => errors.onUnexpectedError(reason)); + + // Contextmenu via IPC support + registerContextMenuListener(); app.on('will-quit', () => { this.logService.trace('App#will-quit: disposing resources'); @@ -117,46 +130,49 @@ export class CodeApplication { } }); - const isValidWebviewSource = (source: string): boolean => { - if (!source) { - return false; - } - if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') { - return true; - } - const srcUri: any = URI.parse(source.toLowerCase()).toString(); - return srcUri.startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString()); - }; - + // Security related measures (https://electronjs.org/docs/tutorial/security) + // DO NOT CHANGE without consulting the documentation app.on('web-contents-created', (event: any, contents) => { contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => { + + // Ensure defaults delete webPreferences.preload; webPreferences.nodeIntegration = false; // Verify URLs being loaded - if (isValidWebviewSource(params.src) && isValidWebviewSource(webPreferences.preloadURL)) { + if (this.isValidWebviewSource(params.src) && this.isValidWebviewSource(webPreferences.preloadURL)) { return; } + delete webPreferences.preloadUrl; + // Otherwise prevent loading this.logService.error('webContents#web-contents-created: Prevented webview attach'); + event.preventDefault(); }); contents.on('will-navigate', event => { this.logService.error('webContents#will-navigate: Prevented webcontent navigation'); + event.preventDefault(); }); + + contents.on('new-window', (event: Event, url: string) => { + event.preventDefault(); // prevent code that wants to open links + + shell.openExternal(url); + }); }); - let macOpenFiles: string[] = []; + let macOpenFileURIs: URI[] = []; let runningTimeout: number = null; app.on('open-file', (event: Event, path: string) => { this.logService.trace('App#open-file: ', path); event.preventDefault(); // Keep in array because more might come! - macOpenFiles.push(path); + macOpenFileURIs.push(URI.file(path)); // Clear previous handler if any if (runningTimeout !== null) { @@ -170,10 +186,10 @@ export class CodeApplication { this.windowsMainService.open({ context: OpenContext.DOCK /* can also be opening from finder while app is running */, cli: this.environmentService.args, - pathsToOpen: macOpenFiles, + urisToOpen: macOpenFileURIs, preferNewWindow: true /* dropping on the dock or opening from finder prefers to open in a new window */ }); - macOpenFiles = []; + macOpenFileURIs = []; runningTimeout = null; } }, 100); @@ -183,15 +199,15 @@ export class CodeApplication { this.windowsMainService.openNewWindow(OpenContext.DESKTOP); //macOS native tab "+" button }); - ipc.on('vscode:exit', (event: any, code: number) => { + ipc.on('vscode:exit', (event: Event, code: number) => { this.logService.trace('IPC#vscode:exit', code); this.dispose(); this.lifecycleService.kill(code); }); - ipc.on('vscode:fetchShellEnv', event => { - const webContents = event.sender.webContents; + ipc.on('vscode:fetchShellEnv', (event: Event) => { + const webContents = event.sender; getShellEnvironment().then(shellEnv => { if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', shellEnv); @@ -205,7 +221,7 @@ export class CodeApplication { }); }); - ipc.on('vscode:broadcast', (event: any, windowId: number, broadcast: { channel: string; payload: any; }) => { + ipc.on('vscode:broadcast', (event: Event, windowId: number, broadcast: { channel: string; payload: any; }) => { if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) { this.logService.trace('IPC#vscode:broadcast', broadcast.channel, broadcast.payload); @@ -217,6 +233,22 @@ export class CodeApplication { } }); + ipc.on('vscode:labelRegisterFormatter', (event: any, data: RegisterFormatterEvent) => { + this.labelService.registerFormatter(data.scheme, data.formatter); + }); + + ipc.on('vscode:toggleDevTools', (event: Event) => { + event.sender.toggleDevTools(); + }); + + ipc.on('vscode:openDevTools', (event: Event) => { + event.sender.openDevTools(); + }); + + ipc.on('vscode:reloadWindow', (event: Event) => { + event.sender.reload(); + }); + // Keyboard layout changes KeyboardLayoutMonitor.INSTANCE.onDidChangeKeyboardLayout(() => { if (this.windowsMainService) { @@ -225,6 +257,20 @@ export class CodeApplication { }); } + private isValidWebviewSource(source: string): boolean { + if (!source) { + return false; + } + + if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') { + return true; + } + + const srcUri: any = URI.parse(source.toLowerCase()).fsPath; + const rootUri = URI.file(this.environmentService.appRoot.toLowerCase()).fsPath; + return srcUri.startsWith(rootUri + nativeSep); + } + private onUnexpectedError(err: Error): void { if (err) { @@ -252,12 +298,12 @@ export class CodeApplication { if (event === 'vscode:changeColorTheme' && typeof payload === 'string') { let data = JSON.parse(payload); - this.stateService.setItem(CodeWindow.themeStorageKey, data.id); - this.stateService.setItem(CodeWindow.themeBackgroundStorageKey, data.background); + this.stateService.setItem(THEME_STORAGE_KEY, data.baseTheme); + this.stateService.setItem(THEME_BG_STORAGE_KEY, data.background); } } - public startup(): TPromise { + startup(): TPromise { this.logService.debug('Starting VS Code'); this.logService.debug(`from: ${this.environmentService.appRoot}`); this.logService.debug('args:', this.environmentService.args); @@ -270,6 +316,20 @@ export class CodeApplication { app.setAppUserModelId(product.win32AppUserModelId); } + // Fix native tabs on macOS 10.13 + // macOS enables a compatibility patch for any bundle ID beginning with + // "com.microsoft.", which breaks native tabs for VS Code when using this + // identifier (from the official build). + // Explicitly opt out of the patch here before creating any windows. + // See: https://github.com/Microsoft/vscode/issues/35361#issuecomment-399794085 + try { + if (platform.isMacintosh && this.configurationService.getValue('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) { + systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any); + } + } catch (error) { + this.logService.error(error); + } + // Create Electron IPC Server this.electronIpcServer = new ElectronIPCServer(); @@ -279,7 +339,7 @@ export class CodeApplication { this.logService.trace(`Resolved machine identifier: ${machineId}`); // Spawn shared process - this.sharedProcess = new SharedProcess(this.environmentService, this.lifecycleService, this.logService, machineId, this.userEnv); + this.sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); // Services @@ -340,11 +400,12 @@ export class CodeApplication { services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess)); services.set(ILaunchService, new SyncDescriptor(LaunchService)); services.set(IIssueService, new SyncDescriptor(IssueService, machineId, this.userEnv)); + services.set(IMenubarService, new SyncDescriptor(MenubarService)); // Telemtry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender'))); - const appender = new TelemetryAppenderClient(channel); + const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)); const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath); const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths }; @@ -381,7 +442,11 @@ export class CodeApplication { const windowsService = accessor.get(IWindowsService); const windowsChannel = new WindowsChannel(windowsService); this.electronIpcServer.registerChannel('windows', windowsChannel); - this.sharedProcessClient.done(client => client.registerChannel('windows', windowsChannel)); + this.sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); + + const menubarService = accessor.get(IMenubarService); + const menubarChannel = new MenubarChannel(menubarService); + this.electronIpcServer.registerChannel('menubar', menubarChannel); const urlService = accessor.get(IURLService); const urlChannel = new URLServiceChannel(urlService); @@ -390,7 +455,7 @@ export class CodeApplication { // Log level management const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService)); this.electronIpcServer.registerChannel('loglevel', logLevelChannel); - this.sharedProcessClient.done(client => client.registerChannel('loglevel', logLevelChannel)); + this.sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); // Lifecycle this.lifecycleService.ready(); @@ -402,7 +467,8 @@ export class CodeApplication { // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(windowsService); - const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { route: () => activeWindowManager.activeClientId }); + const route = () => activeWindowManager.getActiveClientId(); + const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { routeCall: route, routeEvent: route }); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); // On Mac, Code can be running without any open windows, so we must create a window to handle urls, @@ -411,7 +477,7 @@ export class CodeApplication { const environmentService = accessor.get(IEnvironmentService); urlService.registerHandler({ - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { if (windowsMainService.getWindowCount() === 0) { const cli = { ...environmentService.args, goto: true }; const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true }); @@ -419,7 +485,7 @@ export class CodeApplication { return window.ready().then(() => urlService.open(uri)); } - return false; + return TPromise.as(false); } }); } @@ -437,17 +503,20 @@ export class CodeApplication { // Open our first window const macOpenFiles = (global).macOpenFiles as string[]; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; - if (args['new-window'] && args._.length === 0) { + const hasCliArgs = hasArgs(args._); + const hasFolderURIs = hasArgs(args['folder-uri']); + const hasFileURIs = hasArgs(args['file-uri']); + + if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, initialStartup: true }); // new window if "-n" was used without paths - } else if (macOpenFiles && macOpenFiles.length && (!args._ || !args._.length)) { - this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, pathsToOpen: macOpenFiles, initialStartup: true }); // mac: open-file event received on startup + } else if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { + this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => URI.file(file)), initialStartup: true }); // mac: open-file event received on startup } else { - this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!args._.length && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli + this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli } } private afterWindowOpen(accessor: ServicesAccessor): void { - const appInstantiationService = accessor.get(IInstantiationService); const windowsMainService = accessor.get(IWindowsMainService); let windowsMutex: Mutex = null; @@ -487,15 +556,30 @@ export class CodeApplication { } } + // TODO@sbatten: Remove when switching back to dynamic menu // Install Menu - appInstantiationService.createInstance(CodeMenu); + const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); + + let createNativeMenu = true; + if (platform.isLinux) { + createNativeMenu = configurationService.getValue('window.titleBarStyle') !== 'custom'; + } else if (platform.isWindows) { + createNativeMenu = configurationService.getValue('window.titleBarStyle') === 'native'; + } + + if (createNativeMenu) { + instantiationService.createInstance(CodeMenu); + } // Jump List this.historyMainService.updateWindowsJumpList(); this.historyMainService.onRecentlyOpenedChange(() => this.historyMainService.updateWindowsJumpList()); - // Start shared process here - this.sharedProcess.spawn(); + // Start shared process after a while + const sharedProcess = new RunOnceScheduler(() => this.sharedProcess.spawn(), 3000); + sharedProcess.schedule(); + this.toDispose.push(sharedProcess); } private dispose(): void { diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 5b2e51b8bba..229c4e77e0d 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -14,8 +14,8 @@ import { BrowserWindow, app } from 'electron'; type LoginEvent = { event: Electron.Event; webContents: Electron.WebContents; - req: Electron.LoginRequest; - authInfo: Electron.LoginAuthInfo; + req: Electron.Request; + authInfo: Electron.AuthInfo; cb: (username: string, password: string) => void; }; diff --git a/src/vs/code/electron-main/diagnostics.ts b/src/vs/code/electron-main/diagnostics.ts deleted file mode 100644 index 3db04d06bdc..00000000000 --- a/src/vs/code/electron-main/diagnostics.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. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { WorkspaceStats, collectWorkspaceStats, collectLaunchConfigs, WorkspaceStatItem } from 'vs/base/node/stats'; -import { IMainProcessInfo } from 'vs/code/electron-main/launch'; -import { ProcessItem, listProcesses } from 'vs/base/node/ps'; -import product from 'vs/platform/node/product'; -import pkg from 'vs/platform/node/package'; -import * as os from 'os'; -import { virtualMachineHint } from 'vs/base/node/id'; -import { repeat, pad } from 'vs/base/common/strings'; -import { isWindows } from 'vs/base/common/platform'; -import { app } from 'electron'; -import { basename } from 'path'; - -export interface VersionInfo { - vscodeVersion: string; - os: string; -} - -export interface SystemInfo { - CPUs?: string; - 'Memory (System)': string; - 'Load (avg)'?: string; - VM: string; - 'Screen Reader': string; - 'Process Argv': string; - 'GPU Status': Electron.GPUFeatureStatus; -} - -export interface ProcessInfo { - cpu: number; - memory: number; - pid: number; - name: string; -} - -export interface PerformanceInfo { - processInfo?: string; - workspaceInfo?: string; -} - -export function getPerformanceInfo(info: IMainProcessInfo): Promise { - return listProcesses(info.mainPID).then(rootProcess => { - const workspaceInfoMessages = []; - - // Workspace Stats - const workspaceStatPromises = []; - if (info.windows.some(window => window.folders && window.folders.length > 0)) { - info.windows.forEach(window => { - if (window.folders.length === 0) { - return; - } - - workspaceInfoMessages.push(`| Window (${window.title})`); - - window.folders.forEach(folder => { - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`); - workspaceInfoMessages.push(formatWorkspaceStats(stats)); - - const launchConfigs = await collectLaunchConfigs(folder); - if (launchConfigs.length > 0) { - workspaceInfoMessages.push(formatLaunchConfigs(launchConfigs)); - } - })); - }); - }); - } - - return Promise.all(workspaceStatPromises).then(() => { - return { - processInfo: formatProcessList(info, rootProcess), - workspaceInfo: workspaceInfoMessages.join('\n') - }; - }).catch(error => { - return { - processInfo: formatProcessList(info, rootProcess), - workspaceInfo: `Unable to calculate workspace stats: ${error}` - }; - }); - }); -} - -export function getSystemInfo(info: IMainProcessInfo): SystemInfo { - const MB = 1024 * 1024; - const GB = 1024 * MB; - - const systemInfo: SystemInfo = { - 'Memory (System)': `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`, - VM: `${Math.round((virtualMachineHint.value() * 100))}%`, - 'Screen Reader': `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`, - 'Process Argv': `${info.mainArguments.join(' ')}`, - 'GPU Status': app.getGPUFeatureStatus() - }; - - const cpus = os.cpus(); - if (cpus && cpus.length > 0) { - systemInfo.CPUs = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`; - } - - if (!isWindows) { - systemInfo['Load (avg)'] = `${os.loadavg().map(l => Math.round(l)).join(', ')}`; - } - - - return systemInfo; -} - -export function printDiagnostics(info: IMainProcessInfo): Promise { - return listProcesses(info.mainPID).then(rootProcess => { - - // Environment Info - console.log(''); - console.log(formatEnvironment(info)); - - // Process List - console.log(''); - console.log(formatProcessList(info, rootProcess)); - - // Workspace Stats - const workspaceStatPromises = []; - if (info.windows.some(window => window.folders && window.folders.length > 0)) { - console.log(''); - console.log('Workspace Stats: '); - info.windows.forEach(window => { - if (window.folders.length === 0) { - return; - } - - console.log(`| Window (${window.title})`); - - window.folders.forEach(folder => { - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - console.log(`| Folder (${basename(folder)}): ${countMessage}`); - console.log(formatWorkspaceStats(stats)); - - await collectLaunchConfigs(folder).then(launchConfigs => { - if (launchConfigs.length > 0) { - console.log(formatLaunchConfigs(launchConfigs)); - } - }); - }).catch(error => { - console.log(`| Error: Unable to collect workpsace stats for folder ${folder} (${error.toString()})`); - })); - }); - }); - } - - return Promise.all(workspaceStatPromises).then(() => { - console.log(''); - console.log(''); - }); - }); -} - -function formatWorkspaceStats(workspaceStats: WorkspaceStats): string { - const output: string[] = []; - const lineLength = 60; - let col = 0; - - const appendAndWrap = (name: string, count: number) => { - const item = ` ${name}(${count})`; - - if (col + item.length > lineLength) { - output.push(line); - line = '| '; - col = line.length; - } - else { - col += item.length; - } - line += item; - }; - - // File Types - let line = '| File types:'; - const maxShown = 10; - let max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length; - for (let i = 0; i < max; i++) { - const item = workspaceStats.fileTypes[i]; - appendAndWrap(item.name, item.count); - } - output.push(line); - - // Conf Files - if (workspaceStats.configFiles.length >= 0) { - line = '| Conf files:'; - col = 0; - workspaceStats.configFiles.forEach((item) => { - appendAndWrap(item.name, item.count); - }); - output.push(line); - } - - return output.join('\n'); -} - -function formatLaunchConfigs(configs: WorkspaceStatItem[]): string { - const output: string[] = []; - let line = '| Launch Configs:'; - configs.forEach(each => { - const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`; - line += item; - }); - output.push(line); - return output.join('\n'); -} - -function expandGPUFeatures(): string { - const gpuFeatures = app.getGPUFeatureStatus(); - const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length)); - // Make columns aligned by adding spaces after feature name - return Object.keys(gpuFeatures).map(feature => `${feature}: ${repeat(' ', longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n '); -} - -export function formatEnvironment(info: IMainProcessInfo): string { - const MB = 1024 * 1024; - const GB = 1024 * MB; - - const output: string[] = []; - output.push(`Version: ${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`); - output.push(`OS Version: ${os.type()} ${os.arch()} ${os.release()}`); - const cpus = os.cpus(); - if (cpus && cpus.length > 0) { - output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`); - } - output.push(`Memory (System): ${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`); - if (!isWindows) { - output.push(`Load (avg): ${os.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS - } - output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`); - output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`); - output.push(`Process Argv: ${info.mainArguments.join(' ')}`); - output.push(`GPU Status: ${expandGPUFeatures()}`); - - return output.join('\n'); -} - -function formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string { - const mapPidToWindowTitle = new Map(); - info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); - - const output: string[] = []; - - output.push('CPU %\tMem MB\t PID\tProcess'); - - if (rootProcess) { - formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0); - } - - return output.join('\n'); -} - -function formatProcessItem(mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { - const isRoot = (indent === 0); - - const MB = 1024 * 1024; - - // Format name with indent - let name: string; - if (isRoot) { - name = `${product.applicationName} main`; - } else { - name = `${repeat(' ', indent)} ${item.name}`; - - if (item.name === 'window') { - name = `${name} (${mapPidToWindowTitle.get(item.pid)})`; - } - } - const memory = process.platform === 'win32' ? item.mem : (os.totalmem() * (item.mem / 100)); - output.push(`${pad(Number(item.load.toFixed(0)), 5, ' ')}\t${pad(Number((memory / MB).toFixed(0)), 6, ' ')}\t${pad(Number((item.pid).toFixed(0)), 6, ' ')}\t${name}`); - - // Recurse into children if any - if (Array.isArray(item.children)) { - item.children.forEach(child => formatProcessItem(mapPidToWindowTitle, output, child, indent + 1)); - } -} diff --git a/src/vs/code/electron-main/launch.ts b/src/vs/code/electron-main/launch.ts deleted file mode 100644 index 57f56884a01..00000000000 --- a/src/vs/code/electron-main/launch.ts +++ /dev/null @@ -1,283 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IURLService } from 'vs/platform/url/common/url'; -import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { OpenContext, IWindowSettings } from 'vs/platform/windows/common/windows'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { whenDeleted } from 'vs/base/node/pfs'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; -import { Schemas } from 'vs/base/common/network'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import URI from 'vs/base/common/uri'; - -export const ID = 'launchService'; -export const ILaunchService = createDecorator(ID); - -export interface IStartArguments { - args: ParsedArgs; - userEnv: IProcessEnvironment; -} - -export interface IWindowInfo { - pid: number; - title: string; - folders: string[]; -} - -export interface IMainProcessInfo { - mainPID: number; - mainArguments: string[]; - windows: IWindowInfo[]; -} - -function parseOpenUrl(args: ParsedArgs): URI[] { - if (args['open-url'] && args._urls && args._urls.length > 0) { - // --open-url must contain -- followed by the url(s) - // process.argv is used over args._ as args._ are resolved to file paths at this point - return args._urls - .map(url => { - try { - return URI.parse(url); - } catch (err) { - return null; - } - }) - .filter(uri => !!uri); - } - - return []; -} - -export interface ILaunchService { - _serviceBrand: any; - start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise; - getMainProcessId(): TPromise; - getMainProcessInfo(): TPromise; - getLogsPath(): TPromise; -} - -export interface ILaunchChannel extends IChannel { - call(command: 'start', arg: IStartArguments): TPromise; - call(command: 'get-main-process-id', arg: null): TPromise; - call(command: 'get-main-process-info', arg: null): TPromise; - call(command: 'get-logs-path', arg: null): TPromise; - call(command: string, arg: any): TPromise; -} - -export class LaunchChannel implements ILaunchChannel { - - constructor(private service: ILaunchService) { } - - public call(command: string, arg: any): TPromise { - switch (command) { - case 'start': - const { args, userEnv } = arg as IStartArguments; - return this.service.start(args, userEnv); - - case 'get-main-process-id': - return this.service.getMainProcessId(); - - case 'get-main-process-info': - return this.service.getMainProcessInfo(); - - case 'get-logs-path': - return this.service.getLogsPath(); - } - - return undefined; - } -} - -export class LaunchChannelClient implements ILaunchService { - - _serviceBrand: any; - - constructor(private channel: ILaunchChannel) { } - - public start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { - return this.channel.call('start', { args, userEnv }); - } - - public getMainProcessId(): TPromise { - return this.channel.call('get-main-process-id', null); - } - - public getMainProcessInfo(): TPromise { - return this.channel.call('get-main-process-info', null); - } - - public getLogsPath(): TPromise { - return this.channel.call('get-logs-path', null); - } -} - -export class LaunchService implements ILaunchService { - - _serviceBrand: any; - - constructor( - @ILogService private logService: ILogService, - @IWindowsMainService private windowsMainService: IWindowsMainService, - @IURLService private urlService: IURLService, - @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { } - - public start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { - this.logService.trace('Received data from other instance: ', args, userEnv); - - const urlsToOpen = parseOpenUrl(args); - - // Check early for open-url which is handled in URL service - if (urlsToOpen.length) { - let whenWindowReady = TPromise.as(null); - - // Create a window if there is none - if (this.windowsMainService.getWindowCount() === 0) { - const window = this.windowsMainService.openNewWindow(OpenContext.DESKTOP)[0]; - whenWindowReady = window.ready(); - } - - // Make sure a window is open, ready to receive the url event - whenWindowReady.then(() => { - for (const url of urlsToOpen) { - this.urlService.open(url); - } - }); - - return TPromise.as(null); - } - - // Otherwise handle in windows service - return this.startOpenWindow(args, userEnv); - } - - private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { - const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; - let usedWindows: ICodeWindow[]; - - // Special case extension development - if (!!args.extensionDevelopmentPath) { - this.windowsMainService.openExtensionDevelopmentHostWindow({ context, cli: args, userEnv }); - } - - // Start without file/folder arguments - else if (args._.length === 0) { - let openNewWindow = false; - - // Force new window - if (args['new-window'] || args['unity-launch']) { - openNewWindow = true; - } - - // Force reuse window - else if (args['reuse-window']) { - openNewWindow = false; - } - - // Otherwise check for settings - else { - const windowConfig = this.configurationService.getValue('window'); - const openWithoutArgumentsInNewWindowConfig = (windowConfig && windowConfig.openWithoutArgumentsInNewWindow) || 'default' /* default */; - switch (openWithoutArgumentsInNewWindowConfig) { - case 'on': - openNewWindow = true; - break; - case 'off': - openNewWindow = false; - break; - default: - openNewWindow = !isMacintosh; // prefer to restore running instance on macOS - } - } - - if (openNewWindow) { - usedWindows = this.windowsMainService.open({ context, cli: args, userEnv, forceNewWindow: true, forceEmpty: true }); - } else { - usedWindows = [this.windowsMainService.focusLastActive(args, context)]; - } - } - - // Start with file/folder arguments - else { - usedWindows = this.windowsMainService.open({ - context, - cli: args, - userEnv, - forceNewWindow: args['new-window'], - preferNewWindow: !args['reuse-window'] && !args.wait, - forceReuseWindow: args['reuse-window'], - diffMode: args.diff, - addMode: args.add - }); - } - - // If the other instance is waiting to be killed, we hook up a window listener if one window - // is being used and only then resolve the startup promise which will kill this second instance. - // In addition, we poll for the wait marker file to be deleted to return. - if (args.wait && usedWindows.length === 1 && usedWindows[0]) { - return TPromise.any([ - this.windowsMainService.waitForWindowCloseOrLoad(usedWindows[0].id), - whenDeleted(args.waitMarkerFilePath) - ]).then(() => void 0, () => void 0); - } - - return TPromise.as(null); - } - - public getMainProcessId(): TPromise { - this.logService.trace('Received request for process ID from other instance.'); - - return TPromise.as(process.pid); - } - - public getMainProcessInfo(): TPromise { - this.logService.trace('Received request for main process info from other instance.'); - - return TPromise.wrap({ - mainPID: process.pid, - mainArguments: process.argv, - windows: this.windowsMainService.getWindows().map(window => { - return this.getWindowInfo(window); - }) - } as IMainProcessInfo); - } - - public getLogsPath(): TPromise { - this.logService.trace('Received request for logs path from other instance.'); - - return TPromise.as(this.environmentService.logsPath); - } - - private getWindowInfo(window: ICodeWindow): IWindowInfo { - const folders: string[] = []; - - if (window.openedFolderPath) { - folders.push(window.openedFolderPath); - } else if (window.openedWorkspace) { - const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders; - rootFolders.forEach(root => { - if (root.uri.scheme === Schemas.file) { // todo@remote signal remote folders? - folders.push(root.uri.fsPath); - } - }); - } - - return { - pid: window.win.webContents.getOSProcessId(), - title: window.win.getTitle(), - folders - } as IWindowInfo; - } -} diff --git a/src/vs/code/electron-main/logUploader.ts b/src/vs/code/electron-main/logUploader.ts index cf76bbbcd1b..38caf8716a4 100644 --- a/src/vs/code/electron-main/logUploader.ts +++ b/src/vs/code/electron-main/logUploader.ts @@ -11,12 +11,13 @@ import * as fs from 'fs'; import * as path from 'path'; import { localize } from 'vs/nls'; -import { ILaunchChannel } from 'vs/code/electron-main/launch'; +import { ILaunchChannel } from 'vs/platform/launch/electron-main/launchService'; import { TPromise } from 'vs/base/common/winjs.base'; import product from 'vs/platform/node/product'; import { IRequestService } from 'vs/platform/request/node/request'; import { IRequestContext } from 'vs/base/node/request'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CancellationToken } from 'vs/base/common/cancellation'; interface PostResult { readonly blob_id: string; @@ -37,7 +38,7 @@ export async function uploadLogs( channel: ILaunchChannel, requestService: IRequestService, environmentService: IEnvironmentService -): TPromise { +): Promise { const endpoint = Endpoint.getFromProduct(); if (!endpoint) { console.error(localize('invalidEndpoint', 'Invalid log uploader endpoint')); @@ -75,7 +76,7 @@ async function postLogs( endpoint: Endpoint, outZip: string, requestService: IRequestService -): TPromise { +): Promise { const dotter = setInterval(() => console.log('.'), 5000); let result: IRequestContext; try { @@ -86,7 +87,7 @@ async function postLogs( headers: { 'Content-Type': 'application/zip' } - }); + }, CancellationToken.None); } catch (e) { clearInterval(dotter); console.log(localize('postError', 'Error posting logs: {0}', e)); @@ -126,7 +127,7 @@ function zipLogs( return new TPromise((resolve, reject) => { doZip(logsPath, outZip, tempDir, (err, stdout, stderr) => { if (err) { - console.error(localize('zipError', 'Error zipping logs: {0}', err)); + console.error(localize('zipError', 'Error zipping logs: {0}', err.message)); reject(err); } else { resolve(outZip); @@ -152,4 +153,4 @@ function doZip( default: return cp.execFile('zip', ['-r', outZip, '.'], { cwd: logsPath }, callback); } -} \ No newline at end of file +} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 9eb5162971c..46007f71cf3 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,7 +5,7 @@ 'use strict'; -import 'vs/code/electron-main/contributions'; +import 'vs/code/code.main'; import { app, dialog } from 'electron'; import { assign } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; @@ -17,7 +17,7 @@ import { validatePaths } from 'vs/code/node/paths'; import { LifecycleService, ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { Server, serve, connect } from 'vs/base/parts/ipc/node/ipc.net'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ILaunchChannel, LaunchChannelClient } from 'vs/code/electron-main/launch'; +import { ILaunchChannel, LaunchChannelClient } from 'vs/platform/launch/electron-main/launchService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -35,7 +35,7 @@ import { IRequestService } from 'vs/platform/request/node/request'; import { RequestService } from 'vs/platform/request/electron-main/requestService'; import { IURLService } from 'vs/platform/url/common/url'; import { URLService } from 'vs/platform/url/common/urlService'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { CodeApplication } from 'vs/code/electron-main/app'; import { HistoryMainService } from 'vs/platform/history/electron-main/historyMainService'; import { IHistoryMainService } from 'vs/platform/history/common/history'; @@ -44,12 +44,13 @@ import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces import { localize } from 'vs/nls'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; -import { printDiagnostics } from 'vs/code/electron-main/diagnostics'; +import { IDiagnosticsService, DiagnosticsService } from 'vs/platform/diagnostics/electron-main/diagnosticsService'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { uploadLogs } from 'vs/code/electron-main/logUploader'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CommandLineDialogService } from 'vs/platform/dialogs/node/dialogService'; +import { ILabelService, LabelService } from 'vs/platform/label/common/label'; function createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService { const services = new ServiceCollection(); @@ -57,6 +58,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I const environmentService = new EnvironmentService(args, process.execPath); const consoleLogService = new ConsoleLogMainService(getLogLevel(environmentService)); const logService = new MultiplexLogService([consoleLogService, bufferLogService]); + const labelService = new LabelService(environmentService, undefined); process.once('exit', () => logService.dispose()); @@ -64,6 +66,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I setTimeout(() => cleanupOlderLogs(environmentService).then(null, err => console.error(err)), 10000); services.set(IEnvironmentService, environmentService); + services.set(ILabelService, labelService); services.set(ILogService, logService); services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService)); @@ -74,6 +77,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I services.set(IURLService, new SyncDescriptor(URLService)); services.set(IBackupMainService, new SyncDescriptor(BackupMainService)); services.set(IDialogService, new SyncDescriptor(CommandLineDialogService)); + services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); return new InstantiationService(services, true); } @@ -81,7 +85,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I /** * Cleans up older logs, while keeping the 10 most recent ones. */ -async function cleanupOlderLogs(environmentService: EnvironmentService): TPromise { +async function cleanupOlderLogs(environmentService: EnvironmentService): Promise { const currentLog = path.basename(environmentService.logsPath); const logsRoot = path.dirname(environmentService.logsPath); const children = await readdir(logsRoot); @@ -107,10 +111,11 @@ class ExpectedError extends Error { public readonly isExpected = true; } -function setupIPC(accessor: ServicesAccessor): TPromise { +function setupIPC(accessor: ServicesAccessor): Thenable { const logService = accessor.get(ILogService); const environmentService = accessor.get(IEnvironmentService); const requestService = accessor.get(IRequestService); + const diagnosticsService = accessor.get(IDiagnosticsService); function allowSetForegroundWindow(service: LaunchChannelClient): TPromise { let promise = TPromise.wrap(void 0); @@ -131,7 +136,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { return promise; } - function setup(retry: boolean): TPromise { + function setup(retry: boolean): Thenable { return serve(environmentService.mainIPCHandle).then(server => { // Print --status usage info @@ -158,7 +163,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { return server; }, err => { if (err.code !== 'EADDRINUSE') { - return TPromise.wrapError(err); + return Promise.reject(err); } // Since we are the second instance, we do not want to show the dock @@ -176,7 +181,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { logService.error(msg); client.dispose(); - return TPromise.wrapError(new Error(msg)); + return Promise.reject(new Error(msg)); } // Show a warning dialog after some timeout if it takes long to talk to the other instance @@ -198,14 +203,14 @@ function setupIPC(accessor: ServicesAccessor): TPromise { // Process Info if (environmentService.args.status) { return service.getMainProcessInfo().then(info => { - return printDiagnostics(info).then(() => TPromise.wrapError(new ExpectedError())); + return diagnosticsService.printDiagnostics(info).then(() => Promise.reject(new ExpectedError())); }); } // Log uploader if (typeof environmentService.args['upload-logs'] !== 'undefined') { return uploadLogs(channel, requestService, environmentService) - .then(() => TPromise.wrapError(new ExpectedError())); + .then(() => Promise.reject(new ExpectedError())); } logService.trace('Sending env to running instance...'); @@ -220,7 +225,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { clearTimeout(startupWarningDialogHandle); } - return TPromise.wrapError(new ExpectedError('Sent env to running instance. Terminating...')); + return Promise.reject(new ExpectedError('Sent env to running instance. Terminating...')); }); }, err => { @@ -232,7 +237,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { ); } - return TPromise.wrapError(err); + return Promise.reject(err); } // it happens on Linux and OS X that the pipe is left behind @@ -242,7 +247,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { fs.unlinkSync(environmentService.mainIPCHandle); } catch (e) { logService.warn('Could not delete obsolete instance handle', e); - return TPromise.wrapError(e); + return Promise.reject(e); } return setup(false); @@ -304,7 +309,7 @@ function main() { console.error(err.message); app.exit(1); - return; + return void 0; } // We need to buffer the spdlog logs until we are sure @@ -323,6 +328,11 @@ function main() { VSCODE_NLS_CONFIG: process.env['VSCODE_NLS_CONFIG'], VSCODE_LOGS: process.env['VSCODE_LOGS'] }; + + if (process.env['VSCODE_PORTABLE']) { + instanceEnv['VSCODE_PORTABLE'] = process.env['VSCODE_PORTABLE']; + } + assign(process.env, instanceEnv); // Startup @@ -332,7 +342,7 @@ function main() { bufferLogService.logger = createSpdLogService('main', bufferLogService.getLevel(), environmentService.logsPath); return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnv).startup(); }); - }).done(null, err => instantiationService.invokeFunction(quit, err)); + }).then(null, err => instantiationService.invokeFunction(quit, err)); } main(); diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index a23b42e9f61..4f1efab386d 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -18,11 +18,13 @@ import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import product from 'vs/platform/node/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel, getPathLabel } from 'vs/base/common/labels'; +import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel } from 'vs/base/common/labels'; import { KeybindingsResolver } from 'vs/code/electron-main/keyboard'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { ILabelService } from 'vs/platform/label/common/label'; interface IMenuItemClickHandler { inDevTools: (contents: Electron.WebContents) => void; @@ -55,8 +57,6 @@ export class CodeMenu { private closeFolder: Electron.MenuItem; private closeWorkspace: Electron.MenuItem; - private nativeTabMenuItems: Electron.MenuItem[]; - constructor( @IUpdateService private updateService: IUpdateService, @IInstantiationService instantiationService: IInstantiationService, @@ -65,10 +65,9 @@ export class CodeMenu { @IWindowsService private windowsService: IWindowsService, @IEnvironmentService private environmentService: IEnvironmentService, @ITelemetryService private telemetryService: ITelemetryService, - @IHistoryMainService private historyMainService: IHistoryMainService + @IHistoryMainService private historyMainService: IHistoryMainService, + @ILabelService private labelService: ILabelService ) { - this.nativeTabMenuItems = []; - this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); this.keybindingsResolver = instantiationService.createInstance(KeybindingsResolver); @@ -180,21 +179,12 @@ export class CodeMenu { if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) { this.updateMenu(); } - - // Update specific items that are dependent on window count - else if (this.currentEnableNativeTabs) { - this.nativeTabMenuItems.forEach(item => { - if (item) { - item.enabled = e.newCount > 1; - } - }); - } } private updateWorkspaceMenuItems(): void { const window = this.windowsMainService.getLastActiveWindow(); const isInWorkspaceContext = window && !!window.openedWorkspace; - const isInFolderContext = window && !!window.openedFolderPath; + const isInFolderContext = window && !!window.openedFolderUri; this.closeWorkspace.visible = isInWorkspaceContext; this.closeFolder.visible = !isInWorkspaceContext; @@ -244,6 +234,11 @@ export class CodeMenu { const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu }); this.setDebugMenu(debugMenu); + // Terminal + const terminalMenu = new Menu(); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + this.setTerminalMenu(terminalMenu); + // Mac: Window let macWindowMenuItem: Electron.MenuItem; if (isMacintosh) { @@ -257,10 +252,6 @@ export class CodeMenu { const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); this.setHelpMenu(helpMenu); - // Tasks - const taskMenu = new Menu(); - const taskMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTask', comment: ['&& denotes a mnemonic'] }, "&&Tasks")), submenu: taskMenu }); - this.setTaskMenu(taskMenu); // Menu Structure if (macApplicationMenuItem) { @@ -273,7 +264,7 @@ export class CodeMenu { menubar.append(viewMenuItem); menubar.append(gotoMenuItem); menubar.append(debugMenuItem); - menubar.append(taskMenuItem); + menubar.append(terminalMenuItem); if (macWindowMenuItem) { menubar.append(macWindowMenuItem); @@ -379,7 +370,8 @@ export class CodeMenu { const saveAllFiles = this.createMenuItem(nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), 'workbench.action.files.saveAll'); const autoSaveEnabled = [AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => this.currentAutoSaveSetting === s); - const autoSave = new MenuItem(this.likeAction('vscode.toggleAutoSave', { label: this.mnemonicLabel(nls.localize('miAutoSave', "Auto Save")), type: 'checkbox', checked: autoSaveEnabled, enabled: this.windowsMainService.getWindowCount() > 0, click: () => this.windowsMainService.sendToFocused('vscode.toggleAutoSave') }, false)); + + const autoSave = this.createMenuItem(this.mnemonicLabel(nls.localize('miAutoSave', "Auto Save")), 'workbench.action.toggleAutoSave', this.windowsMainService.getWindowCount() > 0, autoSaveEnabled); const preferences = this.getPreferencesMenu(); @@ -429,14 +421,16 @@ export class CodeMenu { private getPreferencesMenu(): Electron.MenuItem { const settings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings"), 'workbench.action.openSettings'); + const extensions = this.createMenuItem(nls.localize({ key: 'miOpenExtensions', comment: ['&& denotes a mnemonic'] }, '&&Extensions'), 'workbench.view.extensions'); const kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings'); - const keymapExtensions = this.createMenuItem(nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions"), 'workbench.extensions.action.showRecommendedKeymapExtensions'); + const keymapExtensions = this.createMenuItem(nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps"), 'workbench.extensions.action.showRecommendedKeymapExtensions'); const snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets'); const colorThemeSelection = this.createMenuItem(nls.localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme"), 'workbench.action.selectTheme'); const iconThemeSelection = this.createMenuItem(nls.localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme"), 'workbench.action.selectIconTheme'); const preferencesMenu = new Menu(); preferencesMenu.append(settings); + preferencesMenu.append(extensions); preferencesMenu.append(__separator__()); preferencesMenu.append(kebindingSettings); preferencesMenu.append(keymapExtensions); @@ -482,13 +476,16 @@ export class CodeMenu { private createOpenRecentMenuItem(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): Electron.MenuItem { let label: string; - let path: string; - if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { - label = unmnemonicLabel(getPathLabel(workspace, null, this.environmentService)); - path = workspace; + let uri: URI; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + label = unmnemonicLabel(this.labelService.getWorkspaceLabel(workspace, { verbose: true })); + uri = workspace; + } else if (isWorkspaceIdentifier(workspace)) { + label = this.labelService.getWorkspaceLabel(workspace, { verbose: true }); + uri = URI.file(workspace.configPath); } else { - label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); - path = workspace.configPath; + uri = URI.file(workspace); + label = unmnemonicLabel(this.labelService.getUriLabel(uri)); } return new MenuItem(this.likeAction(commandId, { @@ -498,12 +495,13 @@ export class CodeMenu { const success = this.windowsMainService.open({ context: OpenContext.MENU, cli: this.environmentService.args, - pathsToOpen: [path], forceNewWindow: openInNewWindow, + urisToOpen: [uri], + forceNewWindow: openInNewWindow, forceOpenWorkspaceAsFile: isFile }).length > 0; if (!success) { - this.historyMainService.removeFromRecentlyOpened([isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath]); + this.historyMainService.removeFromRecentlyOpened([workspace]); } } }, false)); @@ -513,7 +511,7 @@ export class CodeMenu { return event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); } - private createRoleMenuItem(label: string, commandId: string, role: Electron.MenuItemRole): Electron.MenuItem { + private createRoleMenuItem(label: string, commandId: string, role: any): Electron.MenuItem { const options: Electron.MenuItemConstructorOptions = { label: this.mnemonicLabel(label), role, @@ -652,57 +650,18 @@ export class CodeMenu { // Panels const output = this.createMenuItem(nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output"), 'workbench.action.output.toggleOutput'); const debugConsole = this.createMenuItem(nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console"), 'workbench.debug.action.toggleRepl'); - const integratedTerminal = this.createMenuItem(nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal"), 'workbench.action.terminal.toggleTerminal'); + const terminal = this.createMenuItem(nls.localize({ key: 'miToggleTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), 'workbench.action.terminal.toggleTerminal'); const problems = this.createMenuItem(nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems"), 'workbench.actions.view.problems'); + // Appearance + + const appearanceMenu = new Menu(); + const fullscreen = new MenuItem(this.withKeybinding('workbench.action.toggleFullScreen', { label: this.mnemonicLabel(nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen")), click: () => this.windowsMainService.getLastActiveWindow().toggleFullScreen(), enabled: this.windowsMainService.getWindowCount() > 0 })); const toggleZenMode = this.createMenuItem(nls.localize('miToggleZenMode', "Toggle Zen Mode"), 'workbench.action.toggleZenMode'); const toggleCenteredLayout = this.createMenuItem(nls.localize('miToggleCenteredLayout', "Toggle Centered Layout"), 'workbench.action.toggleCenteredLayout'); const toggleMenuBar = this.createMenuItem(nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar"), 'workbench.action.toggleMenuBar'); - // Editor Layout - - const editorLayoutMenu = new Menu(); - - const splitEditorUp = this.createMenuItem(nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up"), 'workbench.action.splitEditorUp'); - const splitEditorDown = this.createMenuItem(nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down"), 'workbench.action.splitEditorDown'); - const splitEditorLeft = this.createMenuItem(nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left"), 'workbench.action.splitEditorLeft'); - const splitEditorRight = this.createMenuItem(nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right"), 'workbench.action.splitEditorRight'); - - const singleColumnEditorLayout = this.createMenuItem(nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single"), 'workbench.action.editorLayoutSingle'); - const centeredEditorLayout = this.createMenuItem(nls.localize({ key: 'miCenteredEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered"), 'workbench.action.editorLayoutCentered'); - const twoColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns"), 'workbench.action.editorLayoutTwoColumns'); - const threeColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns"), 'workbench.action.editorLayoutThreeColumns'); - const twoRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows"), 'workbench.action.editorLayoutTwoRows'); - const threeRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows"), 'workbench.action.editorLayoutThreeRows'); - const twoByTwoGridEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)"), 'workbench.action.editorLayoutTwoByTwoGrid'); - const twoColumnsRightEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two C&&olumns Right"), 'workbench.action.editorLayoutTwoColumnsRight'); - const twoColumnsBottomEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom"), 'workbench.action.editorLayoutTwoColumnsBottom'); - - const toggleEditorLayout = this.createMenuItem(nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout"), 'workbench.action.toggleEditorGroupLayout'); - - [ - splitEditorUp, - splitEditorDown, - splitEditorLeft, - splitEditorRight, - __separator__(), - singleColumnEditorLayout, - centeredEditorLayout, - twoColumnsEditorLayout, - threeColumnsEditorLayout, - twoRowsEditorLayout, - threeRowsEditorLayout, - twoByTwoGridEditorLayout, - twoColumnsRightEditorLayout, - twoColumnsBottomEditorLayout, - __separator__(), - toggleEditorLayout - ].forEach(item => editorLayoutMenu.append(item)); - - const editorLayout = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout")), submenu: editorLayoutMenu }); - - // Workbench Layout const toggleSidebar = this.createMenuItem(nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar"), 'workbench.action.toggleSidebarVisibility'); let moveSideBarLabel: string; @@ -731,21 +690,82 @@ export class CodeMenu { } const toggleActivtyBar = this.createMenuItem(activityBarLabel, 'workbench.action.toggleActivityBarVisibility'); - // Editor - const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap'); - const toggleMinimap = this.createMenuItem(nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"), 'editor.action.toggleMinimap'); - const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace'); - const toggleRenderControlCharacters = this.createMenuItem(nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"), 'editor.action.toggleRenderControlCharacter'); - - // Zoom const zoomIn = this.createMenuItem(nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In"), 'workbench.action.zoomIn'); const zoomOut = this.createMenuItem(nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "Zoom O&&ut"), 'workbench.action.zoomOut'); const resetZoom = this.createMenuItem(nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom"), 'workbench.action.zoomReset'); + arrays.coalesce([ + fullscreen, + toggleZenMode, + toggleCenteredLayout, + isWindows || isLinux ? toggleMenuBar : void 0, + __separator__(), + moveSidebar, + toggleSidebar, + togglePanel, + toggleStatusbar, + toggleActivtyBar, + __separator__(), + zoomIn, + zoomOut, + resetZoom + ]).forEach(item => appearanceMenu.append(item)); + + const appearance = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance")), submenu: appearanceMenu }); + + // Editor Layout + + const editorLayoutMenu = new Menu(); + + const splitEditorUp = this.createMenuItem(nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up"), 'workbench.action.splitEditorUp'); + const splitEditorDown = this.createMenuItem(nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down"), 'workbench.action.splitEditorDown'); + const splitEditorLeft = this.createMenuItem(nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left"), 'workbench.action.splitEditorLeft'); + const splitEditorRight = this.createMenuItem(nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right"), 'workbench.action.splitEditorRight'); + + const singleColumnEditorLayout = this.createMenuItem(nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single"), 'workbench.action.editorLayoutSingle'); + const twoColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns"), 'workbench.action.editorLayoutTwoColumns'); + const threeColumnsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns"), 'workbench.action.editorLayoutThreeColumns'); + const twoRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows"), 'workbench.action.editorLayoutTwoRows'); + const threeRowsEditorLayout = this.createMenuItem(nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows"), 'workbench.action.editorLayoutThreeRows'); + const twoByTwoGridEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)"), 'workbench.action.editorLayoutTwoByTwoGrid'); + const twoRowsRightEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoRowsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two R&&ows Right"), 'workbench.action.editorLayoutTwoRowsRight'); + const twoColumnsBottomEditorLayout = this.createMenuItem(nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom"), 'workbench.action.editorLayoutTwoColumnsBottom'); + + const toggleEditorLayout = this.createMenuItem(nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout"), 'workbench.action.toggleEditorGroupLayout'); + + [ + splitEditorUp, + splitEditorDown, + splitEditorLeft, + splitEditorRight, + __separator__(), + singleColumnEditorLayout, + twoColumnsEditorLayout, + threeColumnsEditorLayout, + twoRowsEditorLayout, + threeRowsEditorLayout, + twoByTwoGridEditorLayout, + twoRowsRightEditorLayout, + twoColumnsBottomEditorLayout, + __separator__(), + toggleEditorLayout + ].forEach(item => editorLayoutMenu.append(item)); + + const editorLayout = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout")), submenu: editorLayoutMenu }); + + const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap'); + const toggleMinimap = this.createMenuItem(nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap"), 'editor.action.toggleMinimap'); + const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace'); + const toggleRenderControlCharacters = this.createMenuItem(nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters"), 'editor.action.toggleRenderControlCharacter'); + const toggleBreadcrumbs = this.createMenuItem(nls.localize({ key: 'miToggleBreadcrumbs', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breadcrumbs"), 'breadcrumbs.toggle'); + arrays.coalesce([ commands, openView, __separator__(), + appearance, + editorLayout, + __separator__(), explorer, search, scm, @@ -755,29 +775,13 @@ export class CodeMenu { output, problems, debugConsole, - integratedTerminal, - __separator__(), - fullscreen, - toggleZenMode, - toggleCenteredLayout, - isWindows || isLinux ? toggleMenuBar : void 0, - __separator__(), - editorLayout, - __separator__(), - moveSidebar, - toggleSidebar, - togglePanel, - toggleStatusbar, - toggleActivtyBar, + terminal, __separator__(), toggleWordWrap, toggleMinimap, toggleRenderWhitespace, toggleRenderControlCharacters, - __separator__(), - zoomIn, - zoomOut, - resetZoom + toggleBreadcrumbs ]).forEach(item => viewMenu.append(item)); } @@ -860,6 +864,41 @@ export class CodeMenu { ].forEach(item => gotoMenu.append(item)); } + private setTerminalMenu(terminalMenu: Electron.Menu): void { + const newTerminal = this.createMenuItem(nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal"), 'workbench.action.terminal.new'); + const splitTerminal = this.createMenuItem(nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), 'workbench.action.terminal.split'); + + const runActiveFile = this.createMenuItem(nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File"), 'workbench.action.terminal.runActiveFile'); + const runSelectedText = this.createMenuItem(nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text"), 'workbench.action.terminal.runSelectedText'); + + const runTask = this.createMenuItem(nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task..."), 'workbench.action.tasks.runTask'); + const buildTask = this.createMenuItem(nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task..."), 'workbench.action.tasks.build'); + const showTasks = this.createMenuItem(nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks..."), 'workbench.action.tasks.showTasks'); + const restartTask = this.createMenuItem(nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task..."), 'workbench.action.tasks.restartTask'); + const terminateTask = this.createMenuItem(nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task..."), 'workbench.action.tasks.terminate'); + const configureTask = this.createMenuItem(nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks..."), 'workbench.action.tasks.configureTaskRunner'); + const configureBuildTask = this.createMenuItem(nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task..."), 'workbench.action.tasks.configureDefaultBuildTask'); + + const menuItems: MenuItem[] = [ + newTerminal, + splitTerminal, + __separator__(), + runTask, + buildTask, + runActiveFile, + runSelectedText, + __separator__(), + terminateTask, + restartTask, + showTasks, + __separator__(), + configureTask, + configureBuildTask + ]; + + menuItems.forEach(item => terminalMenu.append(item)); + } + private setDebugMenu(debugMenu: Electron.Menu): void { const start = this.createMenuItem(nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging"), 'workbench.action.debug.start'); const startWithoutDebugging = this.createMenuItem(nls.localize({ key: 'miStartWithoutDebugging', comment: ['&& denotes a mnemonic'] }, "Start &&Without Debugging"), 'workbench.action.debug.run'); @@ -916,19 +955,16 @@ export class CodeMenu { const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 }); const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow'); - this.nativeTabMenuItems = []; const nativeTabMenuItems: Electron.MenuItem[] = []; if (this.currentEnableNativeTabs) { - const hasMultipleWindows = this.windowsMainService.getWindowCount() > 1; + nativeTabMenuItems.push(this.createMenuItem(nls.localize('mNewTab', "New Tab"), 'workbench.action.newWindowTab')); - this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', hasMultipleWindows)); - this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', hasMultipleWindows)); - this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', hasMultipleWindows)); - this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', hasMultipleWindows)); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', 'selectPreviousTab')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', 'selectNextTab')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', 'moveTabToNewWindow')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', 'mergeAllWindows')); - nativeTabMenuItems.push(__separator__(), ...this.nativeTabMenuItems); - } else { - this.nativeTabMenuItems = []; + nativeTabMenuItems.push(__separator__(), ...nativeTabMenuItems); } [ @@ -945,7 +981,7 @@ export class CodeMenu { const w = this.windowsMainService.getFocusedWindow(); if (w && w.win) { const contents = w.win.webContents; - if (w.hasHiddenTitleBarStyle() && !w.win.isFullScreen() && !contents.isDevToolsOpened()) { + if (isMacintosh && w.hasHiddenTitleBarStyle() && !w.win.isFullScreen() && !contents.isDevToolsOpened()) { contents.openDevTools({ mode: 'undocked' }); // due to https://github.com/electron/electron/issues/3647 } else { contents.toggleDevTools(); @@ -1034,29 +1070,6 @@ export class CodeMenu { } } - private setTaskMenu(taskMenu: Electron.Menu): void { - const runTask = this.createMenuItem(nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task..."), 'workbench.action.tasks.runTask'); - const buildTask = this.createMenuItem(nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task..."), 'workbench.action.tasks.build'); - const showTasks = this.createMenuItem(nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks..."), 'workbench.action.tasks.showTasks'); - const restartTask = this.createMenuItem(nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task..."), 'workbench.action.tasks.restartTask'); - const terminateTask = this.createMenuItem(nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task..."), 'workbench.action.tasks.terminate'); - const configureTask = this.createMenuItem(nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks..."), 'workbench.action.tasks.configureTaskRunner'); - const configureBuildTask = this.createMenuItem(nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task..."), 'workbench.action.tasks.configureDefaultBuildTask'); - - [ - //__separator__(), - runTask, - buildTask, - __separator__(), - terminateTask, - restartTask, - showTasks, - __separator__(), - configureTask, - configureBuildTask - ].forEach(item => taskMenu.append(item)); - } - private openAccessibilityOptions(): void { const win = new BrowserWindow({ alwaysOnTop: true, @@ -1065,7 +1078,10 @@ export class CodeMenu { width: 450, height: 300, show: true, - title: nls.localize('accessibilityOptionsWindowTitle', "Accessibility Options") + title: nls.localize('accessibilityOptionsWindowTitle', "Accessibility Options"), + webPreferences: { + disableBlinkFeatures: 'Auxclick' + } }); win.setMenuBarVisibility(false); @@ -1129,6 +1145,7 @@ export class CodeMenu { private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): Electron.MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): Electron.MenuItem { const label = this.mnemonicLabel(arg1); + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem, win: Electron.BrowserWindow, event: Electron.Event) => { let commandId = arg2; if (Array.isArray(arg2)) { @@ -1137,6 +1154,7 @@ export class CodeMenu { this.runActionInRenderer(commandId); }; + const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0; const checked = typeof arg4 === 'boolean' ? arg4 : false; diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index 7f0a1fca2b5..49519e62453 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -13,6 +13,8 @@ import { ISharedProcess } from 'vs/platform/windows/electron-main/windows'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { IStateService } from 'vs/platform/state/common/state'; +import { getBackgroundColor } from 'vs/code/electron-main/theme'; export class SharedProcess implements ISharedProcess { @@ -21,11 +23,12 @@ export class SharedProcess implements ISharedProcess { private window: Electron.BrowserWindow; constructor( - private readonly environmentService: IEnvironmentService, - private readonly lifecycleService: ILifecycleService, - private readonly logService: ILogService, private readonly machineId: string, private readonly userEnv: IProcessEnvironment, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IStateService private readonly stateService: IStateService, + @ILogService private readonly logService: ILogService ) { } @@ -33,10 +36,12 @@ export class SharedProcess implements ISharedProcess { private get _whenReady(): TPromise { this.window = new BrowserWindow({ show: false, + backgroundColor: getBackgroundColor(this.stateService), webPreferences: { images: false, webaudio: false, - webgl: false + webgl: false, + disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); const config = assign({ diff --git a/src/vs/code/electron-main/theme.ts b/src/vs/code/electron-main/theme.ts new file mode 100644 index 00000000000..fcee1e72733 --- /dev/null +++ b/src/vs/code/electron-main/theme.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { isWindows, isMacintosh } from 'vs/base/common/platform'; +import { systemPreferences } from 'electron'; +import { IStateService } from 'vs/platform/state/common/state'; + +export const DEFAULT_BG_LIGHT = '#FFFFFF'; +export const DEFAULT_BG_DARK = '#1E1E1E'; +export const DEFAULT_BG_HC_BLACK = '#000000'; + +export const THEME_STORAGE_KEY = 'theme'; +export const THEME_BG_STORAGE_KEY = 'themeBackground'; + +export function getBackgroundColor(stateService: IStateService): string { + if (isWindows && systemPreferences.isInvertedColorScheme()) { + return DEFAULT_BG_HC_BLACK; + } + + let background = stateService.getItem(THEME_BG_STORAGE_KEY, null); + if (!background) { + let baseTheme: string; + if (isWindows && systemPreferences.isInvertedColorScheme()) { + baseTheme = 'hc-black'; + } else { + baseTheme = stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; + } + + background = (baseTheme === 'hc-black') ? DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? DEFAULT_BG_LIGHT : DEFAULT_BG_DARK); + } + + if (isMacintosh && background.toUpperCase() === DEFAULT_BG_DARK) { + background = '#171717'; // https://github.com/electron/electron/issues/5150 + } + + return background; +} \ No newline at end of file diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 5f99b37e2e2..78ced48cf48 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -8,9 +8,9 @@ import * as path from 'path'; import * as objects from 'vs/base/common/objects'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IStateService } from 'vs/platform/state/common/state'; -import { shell, screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron'; import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -23,9 +23,10 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { IBackupMainService } from 'vs/platform/backup/common/backup'; -import { ICommandAction } from 'vs/platform/actions/common/actions'; -import { mark, exportEntries } from 'vs/base/common/performance'; +import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import * as perf from 'vs/base/common/performance'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService'; +import { getBackgroundColor } from 'vs/code/electron-main/theme'; export interface IWindowCreationOptions { state: IWindowState; @@ -55,13 +56,6 @@ interface ITouchBarSegment extends Electron.SegmentedControlSegment { export class CodeWindow implements ICodeWindow { - public static readonly themeStorageKey = 'theme'; - public static readonly themeBackgroundStorageKey = 'themeBackground'; - - private static readonly DEFAULT_BG_LIGHT = '#FFFFFF'; - private static readonly DEFAULT_BG_DARK = '#1E1E1E'; - private static readonly DEFAULT_BG_HC_BLACK = '#000000'; - private static readonly MIN_WIDTH = 200; private static readonly MIN_HEIGHT = 120; @@ -124,25 +118,22 @@ export class CodeWindow implements ICodeWindow { // in case we are maximized or fullscreen, only show later after the call to maximize/fullscreen (see below) const isFullscreenOrMaximized = (this.windowState.mode === WindowMode.Maximized || this.windowState.mode === WindowMode.Fullscreen); - let backgroundColor = this.getBackgroundColor(); - if (isMacintosh && backgroundColor.toUpperCase() === CodeWindow.DEFAULT_BG_DARK) { - backgroundColor = '#171717'; // https://github.com/electron/electron/issues/5150 - } - const options: Electron.BrowserWindowConstructorOptions = { width: this.windowState.width, height: this.windowState.height, x: this.windowState.x, y: this.windowState.y, - backgroundColor, + backgroundColor: getBackgroundColor(this.stateService), minWidth: CodeWindow.MIN_WIDTH, minHeight: CodeWindow.MIN_HEIGHT, show: !isFullscreenOrMaximized, title: product.nameLong, webPreferences: { - 'backgroundThrottling': false, // by default if Code is in the background, intervals and timeouts get throttled, - disableBlinkFeatures: 'Auxclick', // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick) - experimentalFeatures: true + // By default if Code is in the background, intervals and timeouts get throttled, so we + // want to enforce that Code stays in the foreground. This triggers a disable_hidden_ + // flag that Electron provides via patch: + // https://github.com/electron/libchromiumcontent/blob/master/patches/common/chromium/disable_hidden.patch + 'backgroundThrottling': false } }; @@ -167,10 +158,18 @@ export class CodeWindow implements ICodeWindow { } let useCustomTitleStyle = false; - if (isMacintosh && (!windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom')) { + if (isMacintosh) { + useCustomTitleStyle = !windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom'; // Default to custom on macOS + const isDev = !this.environmentService.isBuilt || !!config.extensionDevelopmentPath; - if (!isDev) { - useCustomTitleStyle = true; // not enabled when developing due to https://github.com/electron/electron/issues/3647 + if (isDev) { + useCustomTitleStyle = false; // not enabled when developing due to https://github.com/electron/electron/issues/3647 + } + } else { + if (isLinux) { + useCustomTitleStyle = windowConfig && windowConfig.titleBarStyle === 'custom'; + } else { + useCustomTitleStyle = !windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom'; // Default to custom on Windows } } @@ -181,29 +180,15 @@ export class CodeWindow implements ICodeWindow { if (useCustomTitleStyle) { options.titleBarStyle = 'hidden'; this.hiddenTitleBarStyle = true; + if (!isMacintosh) { + options.frame = false; + } } // Create the browser window. this._win = new BrowserWindow(options); this._id = this._win.id; - // Bug in Electron (https://github.com/electron/electron/issues/10862). On multi-monitor setups, - // it can happen that the position we set to the window is not the correct one on the display. - // To workaround, we ask the window for its position and set it again if not matching. - // This only applies if the window is not fullscreen or maximized and multiple monitors are used. - if (isWindows && !isFullscreenOrMaximized) { - try { - if (screen.getAllDisplays().length > 1) { - const [x, y] = this._win.getPosition(); - if (x !== this.windowState.x || y !== this.windowState.y) { - this._win.setPosition(this.windowState.x, this.windowState.y, false); - } - } - } catch (err) { - this.logService.warn(`Unexpected error fixing window position on windows with multiple windows: ${err}\n${err.stack}`); - } - } - if (useCustomTitleStyle) { this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any } @@ -223,35 +208,35 @@ export class CodeWindow implements ICodeWindow { this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too } - public hasHiddenTitleBarStyle(): boolean { + hasHiddenTitleBarStyle(): boolean { return this.hiddenTitleBarStyle; } - public get isExtensionDevelopmentHost(): boolean { + get isExtensionDevelopmentHost(): boolean { return !!this.config.extensionDevelopmentPath; } - public get isExtensionTestHost(): boolean { + get isExtensionTestHost(): boolean { return !!this.config.extensionTestsPath; } - public get extensionDevelopmentPath(): string { + get extensionDevelopmentPath(): string { return this.config.extensionDevelopmentPath; } - public get config(): IWindowConfiguration { + get config(): IWindowConfiguration { return this.currentConfig; } - public get id(): number { + get id(): number { return this._id; } - public get win(): Electron.BrowserWindow { + get win(): Electron.BrowserWindow { return this._win; } - public setRepresentedFilename(filename: string): void { + setRepresentedFilename(filename: string): void { if (isMacintosh) { this.win.setRepresentedFilename(filename); } else { @@ -259,7 +244,7 @@ export class CodeWindow implements ICodeWindow { } } - public getRepresentedFilename(): string { + getRepresentedFilename(): string { if (isMacintosh) { return this.win.getRepresentedFilename(); } @@ -267,7 +252,7 @@ export class CodeWindow implements ICodeWindow { return this.representedFilename; } - public focus(): void { + focus(): void { if (!this._win) { return; } @@ -279,23 +264,23 @@ export class CodeWindow implements ICodeWindow { this._win.focus(); } - public get lastFocusTime(): number { + get lastFocusTime(): number { return this._lastFocusTime; } - public get backupPath(): string { + get backupPath(): string { return this.currentConfig ? this.currentConfig.backupPath : void 0; } - public get openedWorkspace(): IWorkspaceIdentifier { + get openedWorkspace(): IWorkspaceIdentifier { return this.currentConfig ? this.currentConfig.workspace : void 0; } - public get openedFolderPath(): string { - return this.currentConfig ? this.currentConfig.folderPath : void 0; + get openedFolderUri(): URI { + return this.currentConfig ? this.currentConfig.folderUri : void 0; } - public setReady(): void { + setReady(): void { this._readyState = ReadyState.READY; // inform all waiting promises that we are ready now @@ -304,7 +289,7 @@ export class CodeWindow implements ICodeWindow { } } - public ready(): TPromise { + ready(): TPromise { return new TPromise((c) => { if (this._readyState === ReadyState.READY) { return c(this); @@ -315,7 +300,7 @@ export class CodeWindow implements ICodeWindow { }); } - public get readyState(): ReadyState { + get readyState(): ReadyState { return this._readyState; } @@ -327,7 +312,7 @@ export class CodeWindow implements ICodeWindow { // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => { - this.marketplaceHeadersPromise.done(headers => { + this.marketplaceHeadersPromise.then(headers => { cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) }); }); }); @@ -382,18 +367,28 @@ export class CodeWindow implements ICodeWindow { // App commands support this.registerNavigationListenerOn('app-command', 'browser-backward', 'browser-forward', false); - // Handle code that wants to open links - this._win.webContents.on('new-window', (event: Event, url: string) => { - event.preventDefault(); - - shell.openExternal(url); - }); - // Window Focus this._win.on('focus', () => { this._lastFocusTime = Date.now(); }); + // Window (Un)Maximize + this._win.on('maximize', (e) => { + if (this.currentConfig) { + this.currentConfig.maximized = true; + } + + app.emit('browser-window-maximize', e, this._win); + }); + + this._win.on('unmaximize', (e) => { + if (this.currentConfig) { + this.currentConfig.maximized = false; + } + + app.emit('browser-window-unmaximize', e, this._win); + }); + // Window Fullscreen this._win.on('enter-full-screen', () => { this.sendWhenReady('vscode:enterFullScreen'); @@ -408,16 +403,6 @@ export class CodeWindow implements ICodeWindow { this.logService.warn('[electron event]: fail to load, ', errorDescription); }); - // Prevent any kind of navigation triggered by the user! - // But do not touch this in dev version because it will prevent "Reload" from dev tools - if (this.environmentService.isBuilt) { - this._win.webContents.on('will-navigate', (event: Event) => { - if (event) { - event.preventDefault(); - } - }); - } - // Handle configuration changes this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); @@ -494,7 +479,13 @@ export class CodeWindow implements ICodeWindow { }); } - public load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + addTabbedWindow(window: ICodeWindow): void { + if (isMacintosh) { + this._win.addTabbedWindow(window.win); + } + } + + load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work @@ -535,7 +526,7 @@ export class CodeWindow implements ICodeWindow { } // Load URL - mark('main:loadWindow'); + perf.mark('main:loadWindow'); this._win.loadURL(this.getUrl(configuration)); // Make window visible if it did not open in N seconds because this indicates an error @@ -551,7 +542,7 @@ export class CodeWindow implements ICodeWindow { } } - public reload(configuration?: IWindowConfiguration, cli?: ParsedArgs): void { + reload(configuration?: IWindowConfiguration, cli?: ParsedArgs): void { // If config is not provided, copy our current one if (!configuration) { @@ -605,14 +596,12 @@ export class CodeWindow implements ICodeWindow { windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme(); windowConfiguration.accessibilitySupport = app.isAccessibilitySupportEnabled(); - // Theme - windowConfiguration.baseTheme = this.getBaseTheme(); - windowConfiguration.backgroundColor = this.getBackgroundColor(); + // Title style related + windowConfiguration.maximized = this._win.isMaximized(); + windowConfiguration.frameless = this.hasHiddenTitleBarStyle() && !isMacintosh; - // Perf Counters - windowConfiguration.perfEntries = exportEntries(); - windowConfiguration.perfStartTime = (global).perfStartTime; - windowConfiguration.perfWindowLoadTime = Date.now(); + // Dump Perf Counters + windowConfiguration.perfEntries = perf.exportEntries(); // Config (combination of process.argv and window configuration) const environment = parseArgs(process.argv); @@ -623,35 +612,10 @@ export class CodeWindow implements ICodeWindow { } } - return `${require.toUrl('vs/workbench/electron-browser/bootstrap/index.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; + return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; } - private getBaseTheme(): string { - if (isWindows && systemPreferences.isInvertedColorScheme()) { - return 'hc-black'; - } - - const theme = this.stateService.getItem(CodeWindow.themeStorageKey, 'vs-dark'); - - return theme.split(' ')[0]; - } - - private getBackgroundColor(): string { - if (isWindows && systemPreferences.isInvertedColorScheme()) { - return CodeWindow.DEFAULT_BG_HC_BLACK; - } - - const background = this.stateService.getItem(CodeWindow.themeBackgroundStorageKey, null); - if (!background) { - const baseTheme = this.getBaseTheme(); - - return baseTheme === 'hc-black' ? CodeWindow.DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? CodeWindow.DEFAULT_BG_LIGHT : CodeWindow.DEFAULT_BG_DARK); - } - - return background; - } - - public serializeWindowState(): IWindowState { + serializeWindowState(): IWindowState { if (!this._win) { return defaultWindowState(); } @@ -660,16 +624,24 @@ export class CodeWindow implements ICodeWindow { if (this._win.isFullScreen()) { const display = screen.getDisplayMatching(this.getBounds()); - return { + const defaultState = defaultWindowState(); + + const res = { mode: WindowMode.Fullscreen, display: display ? display.id : void 0, - // still carry over window dimensions from previous sessions! - width: this.windowState.width, - height: this.windowState.height, - x: this.windowState.x, - y: this.windowState.y + // Still carry over window dimensions from previous sessions + // if we can compute it in fullscreen state. + // does not seem possible in all cases on Linux for example + // (https://github.com/Microsoft/vscode/issues/58218) so we + // fallback to the defaults in that case. + width: this.windowState.width || defaultState.width, + height: this.windowState.height || defaultState.height, + x: this.windowState.x || 0, + y: this.windowState.y || 0 }; + + return res; } const state: IWindowState = Object.create(null); @@ -807,14 +779,14 @@ export class CodeWindow implements ICodeWindow { return null; } - public getBounds(): Electron.Rectangle { + getBounds(): Electron.Rectangle { const pos = this._win.getPosition(); const dimension = this._win.getSize(); return { x: pos[0], y: pos[1], width: dimension[0], height: dimension[1] }; } - public toggleFullScreen(): void { + toggleFullScreen(): void { const willBeFullScreen = !this._win.isFullScreen(); // set fullscreen flag on window @@ -889,7 +861,7 @@ export class CodeWindow implements ICodeWindow { } } - public onWindowTitleDoubleClick(): void { + onWindowTitleDoubleClick(): void { // Respect system settings on mac with regards to title click on windows title if (isMacintosh) { @@ -916,25 +888,25 @@ export class CodeWindow implements ICodeWindow { } } - public close(): void { + close(): void { if (this._win) { this._win.close(); } } - public sendWhenReady(channel: string, ...args: any[]): void { + sendWhenReady(channel: string, ...args: any[]): void { this.ready().then(() => { this.send(channel, ...args); }); } - public send(channel: string, ...args: any[]): void { + send(channel: string, ...args: any[]): void { if (this._win) { this._win.webContents.send(channel, ...args); } } - public updateTouchBar(groups: ICommandAction[][]): void { + updateTouchBar(groups: ISerializableCommandAction[][]): void { if (!isMacintosh) { return; // only supported on macOS } @@ -960,15 +932,10 @@ export class CodeWindow implements ICodeWindow { this.touchBarGroups.push(groupTouchBar); } - // Ugly workaround for native crash on macOS 10.12.1. We are not - // leveraging the API for changing the ESC touch bar item. - // See https://github.com/electron/electron/issues/10442 - (this._win)._setEscapeTouchBarItem = () => { }; - this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups })); } - private createTouchBarGroup(items: ICommandAction[] = []): Electron.TouchBarSegmentedControl { + private createTouchBarGroup(items: ISerializableCommandAction[] = []): Electron.TouchBarSegmentedControl { // Group Segments const segments = this.createTouchBarGroupSegments(items); @@ -986,11 +953,11 @@ export class CodeWindow implements ICodeWindow { return control; } - private createTouchBarGroupSegments(items: ICommandAction[] = []): ITouchBarSegment[] { + private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] { const segments: ITouchBarSegment[] = items.map(item => { let icon: Electron.NativeImage; - if (item.iconPath) { - icon = nativeImage.createFromPath(item.iconPath.dark); + if (item.iconLocation && item.iconLocation.dark.scheme === 'file') { + icon = nativeImage.createFromPath(URI.revive(item.iconLocation.dark).fsPath); if (icon.isEmpty()) { icon = void 0; } @@ -1006,7 +973,7 @@ export class CodeWindow implements ICodeWindow { return segments; } - public dispose(): void { + dispose(): void { if (this.showTimeoutHandle) { clearTimeout(this.showTimeoutHandle); } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index f1b2021abc3..a67b887acd4 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -6,7 +6,7 @@ 'use strict'; import { basename, normalize, join, dirname } from 'path'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin, equals } from 'vs/base/common/objects'; @@ -14,29 +14,31 @@ import { IBackupMainService } from 'vs/platform/backup/common/backup'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; 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 { IPathWithLineAndColumn, parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleService, UnloadReason, IWindowUnloadEvent } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, ReadyState, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderPath } from 'vs/code/node/windowsFinder'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/code/node/windowsFinder'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import product from 'vs/platform/node/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { isEqual } from 'vs/base/common/paths'; 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 { TPromise } from 'vs/base/common/winjs.base'; -import { IWorkspacesMainService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, IWorkspaceFolderCreationData, 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'; import { normalizeNFC } from 'vs/base/common/normalization'; -import URI from 'vs/base/common/uri'; -import { Queue } from 'vs/base/common/async'; +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 { endsWith } from 'vs/base/common/strings'; enum WindowError { UNRESPONSIVE, @@ -49,11 +51,15 @@ interface INewWindowState extends ISingleWindowState { interface IWindowState { workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: URI; backupPath: string; uiState: ISingleWindowState; } +interface IBackwardCompatibleWindowState extends IWindowState { + folderPath?: string; +} + interface IWindowsState { lastActiveWindow?: IWindowState; lastPluginDevelopmentHostWindow?: IWindowState; @@ -67,7 +73,7 @@ interface IOpenBrowserWindowOptions { cli?: ParsedArgs; workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: URI; initialStartup?: boolean; @@ -77,6 +83,7 @@ interface IOpenBrowserWindowOptions { filesToWait?: IPathsToWaitFor; forceNewWindow?: boolean; + forceNewTabbedWindow?: boolean; windowToUse?: ICodeWindow; emptyWindowBackupFolder?: string; @@ -88,7 +95,7 @@ interface IPathToOpen extends IPath { workspace?: IWorkspaceIdentifier; // the folder path for a Code instance to open - folderPath?: string; + folderUri?: URI; // the backup spath for a Code instance to use backupPath?: string; @@ -144,7 +151,7 @@ export class WindowsManager implements IWindowsMainService { @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService, @IInstantiationService private instantiationService: IInstantiationService ) { - this.windowsState = this.stateService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + this.windowsState = this.getWindowsState(); if (!Array.isArray(this.windowsState.openedWindows)) { this.windowsState.openedWindows = []; } @@ -153,7 +160,31 @@ export class WindowsManager implements IWindowsMainService { this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this); } - public ready(initialUserEnv: IProcessEnvironment): void { + private getWindowsState(): IWindowsState { + const windowsState = this.stateService.getItem(WindowsManager.windowsStateStorageKey) || { openedWindows: [] }; + if (windowsState.lastActiveWindow) { + windowsState.lastActiveWindow = this.revive(windowsState.lastActiveWindow); + } + if (windowsState.lastPluginDevelopmentHostWindow) { + windowsState.lastPluginDevelopmentHostWindow = this.revive(windowsState.lastPluginDevelopmentHostWindow); + } + if (windowsState.openedWindows) { + windowsState.openedWindows = windowsState.openedWindows.map(windowState => this.revive(windowState)); + } + return windowsState; + } + + private revive(windowState: IWindowState): IWindowState { + if (windowState.folderUri) { + windowState.folderUri = URI.revive(windowState.folderUri); + } + if ((windowState).folderPath) { + windowState.folderUri = URI.file((windowState).folderPath); + } + return windowState; + } + + ready(initialUserEnv: IProcessEnvironment): void { this.initialUserEnv = initialUserEnv; this.registerListeners(); @@ -294,10 +325,10 @@ export class WindowsManager implements IWindowsMainService { } // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderPath)) { + else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { this.windowsState.openedWindows.forEach(o => { const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderPath && isEqual(o.folderPath, win.openedFolderPath, !isLinux /* ignorecase */); + const sameFolder = win.openedFolderUri && o.folderUri && isEqual(o.folderUri, win.openedFolderUri); if (sameWorkspace || sameFolder) { o.uiState = state.uiState; @@ -317,27 +348,28 @@ export class WindowsManager implements IWindowsMainService { private toWindowState(win: ICodeWindow): IWindowState { return { workspace: win.openedWorkspace, - folderPath: win.openedFolderPath, + folderUri: win.openedFolderUri, backupPath: win.backupPath, uiState: win.serializeWindowState() }; } - public open(openConfig: IOpenConfiguration): ICodeWindow[] { + open(openConfig: IOpenConfiguration): ICodeWindow[] { + this.logService.trace('windowsManager#open'); openConfig = this.validateOpenConfig(openConfig); let pathsToOpen = this.getPathsToOpen(openConfig); // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. - let foldersToAdd: IPath[] = []; + let foldersToAdd: URI[] = []; if (openConfig.addMode) { - foldersToAdd = pathsToOpen.filter(path => !!path.folderPath).map(path => ({ filePath: path.folderPath })); - pathsToOpen = pathsToOpen.filter(path => !path.folderPath); + foldersToAdd = pathsToOpen.filter(path => !!path.folderUri).map(path => path.folderUri); + pathsToOpen = pathsToOpen.filter(path => !path.folderUri); } - let filesToOpen = pathsToOpen.filter(path => !!path.filePath && !path.createFilePath); - let filesToCreate = pathsToOpen.filter(path => !!path.filePath && path.createFilePath); + let filesToOpen = pathsToOpen.filter(path => !!path.fileUri && !path.createFilePath); + let filesToCreate = pathsToOpen.filter(path => !!path.fileUri && path.createFilePath); // When run with --diff, take the files to open as files to diff // if there are exactly two files provided. @@ -362,12 +394,12 @@ export class WindowsManager implements IWindowsMainService { // // These are windows to open to show either folders or files (including diffing files or creating them) // - const foldersToOpen = arrays.distinct(pathsToOpen.filter(win => win.folderPath && !win.filePath).map(win => win.folderPath), folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + const foldersToOpen = arrays.distinct(pathsToOpen.filter(win => win.folderUri && !win.fileUri).map(win => win.folderUri), folder => getComparisonKey(folder)); // prevent duplicates // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) // - let foldersToRestore: string[] = []; + let foldersToRestore: URI[] = []; let workspacesToRestore: IWorkspaceIdentifier[] = []; let emptyToRestore: string[] = []; if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) { @@ -377,21 +409,22 @@ export class WindowsManager implements IWindowsMainService { workspacesToRestore.push(...this.workspacesMainService.getUntitledWorkspacesSync()); // collect from previous window session emptyToRestore = this.backupMainService.getEmptyWindowBackupPaths(); - emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => basename(w.backupPath))); // add empty windows with backupPath + emptyToRestore.push(...pathsToOpen.filter(w => !w.workspace && !w.folderUri && w.backupPath).map(w => basename(w.backupPath))); // add empty windows with backupPath emptyToRestore = arrays.distinct(emptyToRestore); // prevent duplicates } // // These are empty windows to open // - const emptyToOpen = pathsToOpen.filter(win => !win.workspace && !win.folderPath && !win.filePath && !win.backupPath).length; + const emptyToOpen = pathsToOpen.filter(win => !win.workspace && !win.folderUri && !win.fileUri && !win.backupPath).length; // Open based on config const usedWindows = this.doOpen(openConfig, workspacesToOpen, workspacesToRestore, foldersToOpen, foldersToRestore, emptyToRestore, emptyToOpen, filesToOpen, filesToCreate, filesToDiff, filesToWait, foldersToAdd); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - let focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && (!openConfig.pathsToOpen || !openConfig.pathsToOpen.length); + + let focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !hasArgs(openConfig.cli._) && !hasArgs(openConfig.cli['file-uri']) && !hasArgs(openConfig.cli['folder-uri']) && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; @@ -410,9 +443,9 @@ export class WindowsManager implements IWindowsMainService { for (let i = usedWindows.length - 1; i >= 0; i--) { const usedWindow = usedWindows[i]; if ( - (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace - (usedWindow.openedFolderPath && foldersToRestore.some(folder => folder === usedWindow.openedFolderPath)) || // skip over restored folder - (usedWindow.backupPath && emptyToRestore.some(empty => empty === basename(usedWindow.backupPath))) // skip over restored empty window + (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace + (usedWindow.openedFolderUri && foldersToRestore.some(folder => isEqual(folder, usedWindow.openedFolderUri))) || // skip over restored folder + (usedWindow.backupPath && emptyToRestore.some(empty => empty === basename(usedWindow.backupPath))) // skip over restored empty window ) { continue; } @@ -433,13 +466,13 @@ export class WindowsManager implements IWindowsMainService { // Also do not add paths when files are opened for diffing, only if opened individually if (!usedWindows.some(w => w.isExtensionDevelopmentHost) && !openConfig.cli.diff) { const recentlyOpenedWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] = []; - const recentlyOpenedFiles: string[] = []; + const recentlyOpenedFiles: URI[] = []; pathsToOpen.forEach(win => { - if (win.workspace || win.folderPath) { - recentlyOpenedWorkspaces.push(win.workspace || win.folderPath); - } else if (win.filePath) { - recentlyOpenedFiles.push(win.filePath); + if (win.workspace || win.folderUri) { + recentlyOpenedWorkspaces.push(win.workspace || win.folderUri); + } else if (win.fileUri) { + recentlyOpenedFiles.push(win.fileUri); } }); @@ -452,7 +485,7 @@ export class WindowsManager implements IWindowsMainService { // used for the edit operation is closed or loaded to a different folder so that the waiting // process can continue. We do this by deleting the waitMarkerFilePath. if (openConfig.context === OpenContext.CLI && openConfig.cli.wait && openConfig.cli.waitMarkerFilePath && usedWindows.length === 1 && usedWindows[0]) { - this.waitForWindowCloseOrLoad(usedWindows[0].id).done(() => fs.unlink(openConfig.cli.waitMarkerFilePath, error => void 0)); + this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(openConfig.cli.waitMarkerFilePath, error => void 0)); } return usedWindows; @@ -472,15 +505,15 @@ export class WindowsManager implements IWindowsMainService { openConfig: IOpenConfiguration, workspacesToOpen: IWorkspaceIdentifier[], workspacesToRestore: IWorkspaceIdentifier[], - foldersToOpen: string[], - foldersToRestore: string[], + foldersToOpen: URI[], + foldersToRestore: URI[], emptyToRestore: string[], emptyToOpen: number, filesToOpen: IPath[], filesToCreate: IPath[], filesToDiff: IPath[], filesToWait: IPathsToWaitFor, - foldersToAdd: IPath[] + foldersToAdd: URI[] ) { const usedWindows: ICodeWindow[] = []; @@ -509,17 +542,10 @@ export class WindowsManager implements IWindowsMainService { newWindow: openFilesInNewWindow, reuseWindow: openConfig.forceReuseWindow, context: openConfig.context, - filePath: fileToCheck && fileToCheck.filePath, - userHome: this.environmentService.userHome, + fileUri: fileToCheck && fileToCheck.fileUri, workspaceResolver: workspace => this.workspacesMainService.resolveWorkspaceSync(workspace.configPath) }); - // Special case: we started with --wait and we got back a folder to open. In this case - // we actually prefer to not open the folder but operate purely on the file. - if (typeof bestWindowOrFolder === 'string' && filesToWait) { - bestWindowOrFolder = !openFilesInNewWindow ? this.getLastActiveWindow() : null; - } - // We found a window to open the files in if (bestWindowOrFolder instanceof CodeWindow) { @@ -529,8 +555,8 @@ export class WindowsManager implements IWindowsMainService { } // Window is single folder - else if (bestWindowOrFolder.openedFolderPath) { - foldersToOpen.push(bestWindowOrFolder.openedFolderPath); + else if (bestWindowOrFolder.openedFolderUri) { + foldersToOpen.push(bestWindowOrFolder.openedFolderUri); } // Window is empty @@ -547,11 +573,6 @@ export class WindowsManager implements IWindowsMainService { } } - // We found a suitable folder to open: add it to foldersToOpen - else if (typeof bestWindowOrFolder === 'string') { - foldersToOpen.push(bestWindowOrFolder); - } - // Finally, if no window or folder is found, just open the files in an empty window else { usedWindows.push(this.openInBrowserWindow({ @@ -562,7 +583,8 @@ export class WindowsManager implements IWindowsMainService { filesToCreate, filesToDiff, filesToWait, - forceNewWindow: true + forceNewWindow: true, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow })); // Reset these because we handled them @@ -614,7 +636,8 @@ export class WindowsManager implements IWindowsMainService { } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = arrays.distinct([...foldersToRestore, ...foldersToOpen], folder => isLinux ? folder : folder.toLowerCase()); // prevent duplicates + const allFoldersToOpen = arrays.distinct([...foldersToRestore, ...foldersToOpen], folder => getComparisonKey(folder)); // prevent duplicates + if (allFoldersToOpen.length > 0) { // Check for existing instances @@ -636,12 +659,13 @@ export class WindowsManager implements IWindowsMainService { // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - if (windowsOnFolderPath.some(win => isEqual(win.openedFolderPath, folderToOpen, !isLinux /* ignorecase */))) { + + if (windowsOnFolderPath.some(win => isEqual(win.openedFolderUri, folderToOpen))) { return; // ignore folders that are already open } // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { folderPath: folderToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff, filesToWait)); + usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, { folderUri: folderToOpen }, openFolderInNewWindow, filesToOpen, filesToCreate, filesToDiff, filesToWait)); // Reset these because we handled them filesToOpen = []; @@ -665,6 +689,7 @@ export class WindowsManager implements IWindowsMainService { filesToDiff, filesToWait, forceNewWindow: true, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow, emptyWindowBackupFolder })); @@ -685,7 +710,8 @@ export class WindowsManager implements IWindowsMainService { userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - forceNewWindow: openFolderInNewWindow + forceNewWindow: openFolderInNewWindow, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow })); openFolderInNewWindow = true; // any other window to open must open in new window then @@ -706,7 +732,7 @@ export class WindowsManager implements IWindowsMainService { return window; } - private doAddFoldersToExistingWidow(window: ICodeWindow, foldersToAdd: IPath[]): ICodeWindow { + private doAddFoldersToExistingWidow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { window.focus(); // make sure window has focus window.ready().then(readyWindow => { @@ -726,12 +752,13 @@ export class WindowsManager implements IWindowsMainService { cli: openConfig.cli, initialStartup: openConfig.initialStartup, workspace: folderOrWorkspace.workspace, - folderPath: folderOrWorkspace.folderPath, + folderUri: folderOrWorkspace.folderUri, filesToOpen, filesToCreate, filesToDiff, filesToWait, forceNewWindow, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow, windowToUse }); @@ -743,7 +770,7 @@ export class WindowsManager implements IWindowsMainService { let isCommandLineOrAPICall = false; // Extract paths: from API - if (openConfig.pathsToOpen && openConfig.pathsToOpen.length > 0) { + if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { windowsToOpen = this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } @@ -754,7 +781,7 @@ export class WindowsManager implements IWindowsMainService { } // Extract paths: from CLI - else if (openConfig.cli._.length > 0) { + else if (hasArgs(openConfig.cli._) || hasArgs(openConfig.cli['folder-uri']) || hasArgs(openConfig.cli['file-uri'])) { windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli); isCommandLineOrAPICall = true; } @@ -769,13 +796,13 @@ export class WindowsManager implements IWindowsMainService { // If we are in addMode, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = windowsToOpen.filter(path => !!path.folderPath); + const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); if (foldersToOpen.length > 1) { - const workspace = this.workspacesMainService.createWorkspaceSync(foldersToOpen.map(folder => ({ uri: URI.file(folder.folderPath) }))); + const workspace = this.workspacesMainService.createWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri }))); // Add workspace and remove folders thereby windowsToOpen.push({ workspace }); - windowsToOpen = windowsToOpen.filter(path => !path.folderPath); + windowsToOpen = windowsToOpen.filter(path => !path.folderUri); } } @@ -783,35 +810,74 @@ export class WindowsManager implements IWindowsMainService { } private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPath[] { - let pathsToOpen = openConfig.pathsToOpen.map(pathToOpen => { - const path = this.parsePath(pathToOpen, { gotoLineMode: openConfig.cli && openConfig.cli.goto, forceOpenWorkspaceAsFile: openConfig.forceOpenWorkspaceAsFile }); + let pathsToOpen = []; + let parseOptions = { gotoLineMode: openConfig.cli && openConfig.cli.goto, forceOpenWorkspaceAsFile: openConfig.forceOpenWorkspaceAsFile }; + for (const pathToOpen of openConfig.urisToOpen) { + if (!pathToOpen) { + continue; + } - // Warn if the requested path to open does not exist - if (!path) { + const path = this.parseUri(pathToOpen, openConfig.forceOpenWorkspaceAsFile, parseOptions); + if (path) { + pathsToOpen.push(path); + } else { + // Warn about the invalid URI or path + + let message, detail; + if (pathToOpen.scheme === Schemas.file) { + message = localize('pathNotExistTitle', "Path does not exist"); + detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen.fsPath); + } else { + message = localize('uriInvalidTitle', "URI can not be opened"); + detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", pathToOpen.toString()); + } const options: Electron.MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message: localize('pathNotExistTitle', "Path does not exist"), - detail: localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen), + message, + detail, noLink: true }; this.dialogs.showMessageBox(options, this.getFocusedWindow()); } - - return path; - }); - - // get rid of nulls - pathsToOpen = arrays.coalesce(pathsToOpen); - + } return pathsToOpen; } private doExtractPathsFromCLI(cli: ParsedArgs): IPath[] { - const pathsToOpen = arrays.coalesce(cli._.map(candidate => this.parsePath(candidate, { ignoreFileNotFound: true, gotoLineMode: cli.goto }))); - if (pathsToOpen.length > 0) { + const pathsToOpen = []; + const parseOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto }; + + // folder uris + const folderUris = asArray(cli['folder-uri']); + for (let folderUri of folderUris) { + const path = this.parseUri(this.argToUri(folderUri), false, parseOptions); + if (path) { + pathsToOpen.push(path); + } + } + + // file uris + const fileUris = asArray(cli['file-uri']); + for (let fileUri of fileUris) { + const path = this.parseUri(this.argToUri(fileUri), true, parseOptions); + if (path) { + pathsToOpen.push(path); + } + } + + // folder or file paths + const cliArgs = asArray(cli._); + for (let cliArg of cliArgs) { + const path = this.parsePath(cliArg, parseOptions); + if (path) { + pathsToOpen.push(path); + } + } + + if (pathsToOpen.length) { return pathsToOpen; } @@ -843,9 +909,9 @@ export class WindowsManager implements IWindowsMainService { } // folder (if path is valid) - else if (lastActiveWindow.folderPath) { - const validatedFolder = this.parsePath(lastActiveWindow.folderPath); - if (validatedFolder && validatedFolder.folderPath) { + else if (lastActiveWindow.folderUri) { + const validatedFolder = this.parseUri(lastActiveWindow.folderUri, false); + if (validatedFolder && validatedFolder.folderUri) { return [validatedFolder]; } } @@ -871,16 +937,16 @@ export class WindowsManager implements IWindowsMainService { windowsToOpen.push(...workspaceCandidates.map(candidate => this.parsePath(candidate.configPath)).filter(window => window && window.workspace)); // Folders - const folderCandidates = this.windowsState.openedWindows.filter(w => !!w.folderPath).map(w => w.folderPath); - if (lastActiveWindow && lastActiveWindow.folderPath) { - folderCandidates.push(lastActiveWindow.folderPath); + const folderCandidates = this.windowsState.openedWindows.filter(w => !!w.folderUri).map(w => w.folderUri); + if (lastActiveWindow && lastActiveWindow.folderUri) { + folderCandidates.push(lastActiveWindow.folderUri); } - windowsToOpen.push(...folderCandidates.map(candidate => this.parsePath(candidate)).filter(window => window && window.folderPath)); + windowsToOpen.push(...folderCandidates.map(candidate => this.parseUri(candidate, false)).filter(window => window && window.folderUri)); // Windows that were Empty if (restoreWindows === 'all') { - const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspace && !w.folderPath && w.backupPath).map(w => w.backupPath); - const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspace && !lastActiveWindow.folderPath && lastActiveWindow.backupPath; + const lastOpenedEmpty = this.windowsState.openedWindows.filter(w => !w.workspace && !w.folderUri && w.backupPath).map(w => w.backupPath); + const lastActiveEmpty = lastActiveWindow && !lastActiveWindow.workspace && !lastActiveWindow.folderUri && lastActiveWindow.backupPath; if (lastActiveEmpty) { lastOpenedEmpty.push(lastActiveEmpty); } @@ -915,6 +981,50 @@ export class WindowsManager implements IWindowsMainService { return restoreWindows; } + private argToUri(arg: string): URI { + try { + let uri = URI.parse(arg); + if (!uri.scheme) { + this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); + return null; + } + return uri; + } catch (e) { + this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + } + return null; + } + + private parseUri(uri: URI, isFile: boolean, options?: { ignoreFileNotFound?: boolean, gotoLineMode?: boolean, forceOpenWorkspaceAsFile?: boolean; }): IPathToOpen { + if (!uri || !uri.scheme) { + return null; + } + if (uri.scheme === Schemas.file) { + return this.parsePath(uri.fsPath, options); + } + // normalize URI + uri = normalizePath(uri); + if (endsWith(uri.path, '/')) { + uri = uri.with({ path: uri.path.substr(0, uri.path.length - 1) }); + } + if (isFile) { + if (options && options.gotoLineMode) { + const parsedPath = parseLineAndColumnAware(uri.path); + return { + fileUri: uri.with({ path: parsedPath.path }), + lineNumber: parsedPath.line, + columnNumber: parsedPath.column + }; + } + return { + fileUri: uri + }; + } + return { + folderUri: uri + }; + } + private parsePath(anyPath: string, options?: { ignoreFileNotFound?: boolean, gotoLineMode?: boolean, forceOpenWorkspaceAsFile?: boolean; }): IPathToOpen { if (!anyPath) { return null; @@ -944,7 +1054,7 @@ export class WindowsManager implements IWindowsMainService { // File return { - filePath: candidate, + fileUri: URI.file(candidate), lineNumber: gotoLineMode ? parsedPath.line : void 0, columnNumber: gotoLineMode ? parsedPath.column : void 0 }; @@ -955,15 +1065,16 @@ export class WindowsManager implements IWindowsMainService { // over to us) else if (candidateStat.isDirectory()) { return { - folderPath: candidate + folderUri: URI.file(candidate) }; } } } catch (error) { - this.historyMainService.removeFromRecentlyOpened([candidate]); // since file does not seem to exist anymore, remove from recent + const fileUri = URI.file(candidate); + this.historyMainService.removeFromRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent if (options && options.ignoreFileNotFound) { - return { filePath: candidate, createFilePath: true }; // assume this is a file that does not yet exist + return { fileUri, createFilePath: true }; // assume this is a file that does not yet exist } } @@ -1011,7 +1122,7 @@ export class WindowsManager implements IWindowsMainService { return { openFolderInNewWindow, openFilesInNewWindow }; } - public openExtensionDevelopmentHostWindow(openConfig: IOpenConfiguration): void { + openExtensionDevelopmentHostWindow(openConfig: IOpenConfiguration): void { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window @@ -1023,23 +1134,46 @@ export class WindowsManager implements IWindowsMainService { return; } + let folderUris = asArray(openConfig.cli['folder-uri']); + let fileUris = asArray(openConfig.cli['file-uri']); + let cliArgs = openConfig.cli._; // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing - if (openConfig.cli._.length === 0 && !openConfig.cli.extensionTestsPath) { + if (!cliArgs.length && !folderUris.length && !fileUris.length && !openConfig.cli.extensionTestsPath) { const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; - const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderPath); + const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { - openConfig.cli._ = [isSingleFolderWorkspaceIdentifier(workspaceToOpen) ? workspaceToOpen : workspaceToOpen.configPath]; + if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { + if (workspaceToOpen.scheme === Schemas.file) { + cliArgs = [workspaceToOpen.fsPath]; + } else { + folderUris = [workspaceToOpen.toString()]; + } + } else { + cliArgs = [workspaceToOpen.configPath]; + } } } // Make sure we are not asked to open a workspace or folder that is already opened - if (openConfig.cli._.some(path => !!findWindowOnWorkspaceOrFolderPath(WindowsManager.WINDOWS, path))) { - openConfig.cli._ = []; + if (cliArgs.length && cliArgs.some(path => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, URI.file(path)))) { + cliArgs = []; } + if (folderUris.length && folderUris.some(uri => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, this.argToUri(uri)))) { + folderUris = []; + } + + if (fileUris.length && fileUris.some(uri => !!findWindowOnWorkspaceOrFolderUri(WindowsManager.WINDOWS, this.argToUri(uri)))) { + fileUris = []; + } + + openConfig.cli._ = cliArgs; + openConfig.cli['folder-uri'] = folderUris; + openConfig.cli['file-uri'] = fileUris; + // Open it - this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: openConfig.cli._.length === 0, userEnv: openConfig.userEnv }); + this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: !cliArgs.length && !folderUris.length && !fileUris.length, userEnv: openConfig.userEnv }); } private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { @@ -1048,11 +1182,12 @@ export class WindowsManager implements IWindowsMainService { const configuration: IWindowConfiguration = mixin({}, options.cli); // inherit all properties from CLI configuration.appRoot = this.environmentService.appRoot; configuration.machineId = this.machineId; + configuration.mainPid = process.pid; configuration.execPath = process.execPath; configuration.userEnv = assign({}, this.initialUserEnv, options.userEnv || {}); configuration.isInitialStartup = options.initialStartup; configuration.workspace = options.workspace; - configuration.folderPath = options.folderPath; + configuration.folderUri = options.folderUri; configuration.filesToOpen = options.filesToOpen; configuration.filesToCreate = options.filesToCreate; configuration.filesToDiff = options.filesToDiff; @@ -1068,7 +1203,7 @@ export class WindowsManager implements IWindowsMainService { } let window: ICodeWindow; - if (!options.forceNewWindow) { + if (!options.forceNewWindow && !options.forceNewTabbedWindow) { window = options.windowToUse || this.getLastActiveWindow(); if (window) { window.focus(); @@ -1095,12 +1230,21 @@ export class WindowsManager implements IWindowsMainService { state.mode = WindowMode.Normal; } + // Create the window window = this.instantiationService.createInstance(CodeWindow, { state, extensionDevelopmentPath: configuration.extensionDevelopmentPath, isExtensionTestHost: !!configuration.extensionTestsPath }); + // Add as window tab if configured (macOS only) + if (options.forceNewTabbedWindow) { + const activeWindow = this.getLastActiveWindow(); + if (activeWindow) { + activeWindow.addTabbedWindow(window); + } + } + // Add to our list of windows WindowsManager.WINDOWS.push(window); @@ -1135,15 +1279,15 @@ export class WindowsManager implements IWindowsMainService { } // Only load when the window has not vetoed this - this.lifecycleService.unload(window, UnloadReason.LOAD).done(veto => { + this.lifecycleService.unload(window, UnloadReason.LOAD).then(veto => { if (!veto) { // Register window for backups if (!configuration.extensionDevelopmentPath) { if (configuration.workspace) { configuration.backupPath = this.backupMainService.registerWorkspaceBackupSync(configuration.workspace); - } else if (configuration.folderPath) { - configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderPath); + } else if (configuration.folderUri) { + configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderUri); } else { configuration.backupPath = this.backupMainService.registerEmptyWindowBackupSync(options.emptyWindowBackupFolder); } @@ -1180,8 +1324,8 @@ export class WindowsManager implements IWindowsMainService { } // Known Folder - load from stored settings - if (configuration.folderPath) { - const stateForFolder = this.windowsState.openedWindows.filter(o => isEqual(o.folderPath, configuration.folderPath, !isLinux /* ignorecase */)).map(o => o.uiState); + if (configuration.folderUri) { + const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); if (stateForFolder.length) { return stateForFolder[0]; } @@ -1287,10 +1431,10 @@ export class WindowsManager implements IWindowsMainService { return state; } - public reload(win: ICodeWindow, cli?: ParsedArgs): void { + reload(win: ICodeWindow, cli?: ParsedArgs): void { // Only reload when the window has not vetoed this - this.lifecycleService.unload(win, UnloadReason.RELOAD).done(veto => { + this.lifecycleService.unload(win, UnloadReason.RELOAD).then(veto => { if (!veto) { win.reload(void 0, cli); @@ -1300,18 +1444,22 @@ export class WindowsManager implements IWindowsMainService { }); } - public closeWorkspace(win: ICodeWindow): void { + closeWorkspace(win: ICodeWindow): void { this.openInBrowserWindow({ cli: this.environmentService.args, windowToUse: win }); } - public saveAndEnterWorkspace(win: ICodeWindow, path: string): TPromise { + saveAndEnterWorkspace(win: ICodeWindow, path: string): TPromise { return this.workspacesManager.saveAndEnterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); } - public createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { + enterWorkspace(win: ICodeWindow, path: string): TPromise { + return this.workspacesManager.enterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); + } + + createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.workspacesManager.createAndEnterWorkspace(win, folders, path).then(result => this.doEnterWorkspace(win, result)); } @@ -1326,7 +1474,7 @@ export class WindowsManager implements IWindowsMainService { return result; } - public pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { + pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { this.workspacesManager.pickWorkspaceAndOpen(options); } @@ -1363,11 +1511,11 @@ export class WindowsManager implements IWindowsMainService { // might be related to the fact that the untitled workspace prompt shows up async and this // code can execute before the dialog is fully closed which then blocks the window from closing. // Issue: https://github.com/Microsoft/vscode/issues/41989 - return TPromise.timeout(0).then(() => veto); + return timeout(0).then(() => veto); })); } - public focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow { + focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow { const lastActive = this.getLastActiveWindow(); if (lastActive) { lastActive.focus(); @@ -1379,15 +1527,19 @@ export class WindowsManager implements IWindowsMainService { return this.open({ context, cli, forceEmpty: true })[0]; } - public getLastActiveWindow(): ICodeWindow { + getLastActiveWindow(): ICodeWindow { return getLastActiveWindow(WindowsManager.WINDOWS); } - public openNewWindow(context: OpenContext): ICodeWindow[] { + openNewWindow(context: OpenContext): ICodeWindow[] { return this.open({ context, cli: this.environmentService.args, forceNewWindow: true, forceEmpty: true }); } - public waitForWindowCloseOrLoad(windowId: number): TPromise { + openNewTabbedWindow(context: OpenContext): ICodeWindow[] { + return this.open({ context, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true }); + } + + waitForWindowCloseOrLoad(windowId: number): TPromise { return new TPromise(c => { function handler(id: number) { if (id === windowId) { @@ -1403,7 +1555,7 @@ export class WindowsManager implements IWindowsMainService { }); } - public sendToFocused(channel: string, ...args: any[]): void { + sendToFocused(channel: string, ...args: any[]): void { const focusedWindow = this.getFocusedWindow() || this.getLastActiveWindow(); if (focusedWindow) { @@ -1411,7 +1563,7 @@ export class WindowsManager implements IWindowsMainService { } } - public sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void { + sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void { WindowsManager.WINDOWS.forEach(w => { if (windowIdsToIgnore && windowIdsToIgnore.indexOf(w.id) >= 0) { return; // do not send if we are instructed to ignore it @@ -1421,7 +1573,7 @@ export class WindowsManager implements IWindowsMainService { }); } - public getFocusedWindow(): ICodeWindow { + getFocusedWindow(): ICodeWindow { const win = BrowserWindow.getFocusedWindow(); if (win) { return this.getWindowById(win.id); @@ -1430,7 +1582,7 @@ export class WindowsManager implements IWindowsMainService { return null; } - public getWindowById(windowId: number): ICodeWindow { + getWindowById(windowId: number): ICodeWindow { const res = WindowsManager.WINDOWS.filter(w => w.id === windowId); if (res && res.length === 1) { return res[0]; @@ -1439,11 +1591,11 @@ export class WindowsManager implements IWindowsMainService { return null; } - public getWindows(): ICodeWindow[] { + getWindows(): ICodeWindow[] { return WindowsManager.WINDOWS; } - public getWindowCount(): number { + getWindowCount(): number { return WindowsManager.WINDOWS.length; } @@ -1511,15 +1663,15 @@ export class WindowsManager implements IWindowsMainService { this._onWindowClose.fire(win.id); } - public pickFileFolderAndOpen(options: INativeOpenDialogOptions): void { + pickFileFolderAndOpen(options: INativeOpenDialogOptions): void { this.doPickAndOpen(options, true /* pick folders */, true /* pick files */); } - public pickFolderAndOpen(options: INativeOpenDialogOptions): void { + pickFolderAndOpen(options: INativeOpenDialogOptions): void { this.doPickAndOpen(options, true /* pick folders */, false /* pick files */); } - public pickFileAndOpen(options: INativeOpenDialogOptions): void { + pickFileAndOpen(options: INativeOpenDialogOptions): void { this.doPickAndOpen(options, false /* pick folders */, true /* pick files */); } @@ -1557,19 +1709,19 @@ export class WindowsManager implements IWindowsMainService { this.dialogs.pickAndOpen(internalOptions); } - public showMessageBox(options: Electron.MessageBoxOptions, win?: ICodeWindow): TPromise { + showMessageBox(options: Electron.MessageBoxOptions, win?: ICodeWindow): TPromise { return this.dialogs.showMessageBox(options, win); } - public showSaveDialog(options: Electron.SaveDialogOptions, win?: ICodeWindow): TPromise { + showSaveDialog(options: Electron.SaveDialogOptions, win?: ICodeWindow): TPromise { return this.dialogs.showSaveDialog(options, win); } - public showOpenDialog(options: Electron.OpenDialogOptions, win?: ICodeWindow): TPromise { + showOpenDialog(options: Electron.OpenDialogOptions, win?: ICodeWindow): TPromise { return this.dialogs.showOpenDialog(options, win); } - public quit(): void { + quit(): void { // If the user selected to exit from an extension development host window, do not quit, but just // close the window unless this is the last window that is opened. @@ -1609,8 +1761,8 @@ class Dialogs { this.noWindowDialogQueue = new Queue(); } - public pickAndOpen(options: INativeOpenDialogOptions): void { - this.getFileOrFolderPaths(options).then(paths => { + pickAndOpen(options: INativeOpenDialogOptions): void { + this.getFileOrFolderUris(options).then(paths => { const numberOfPaths = paths ? paths.length : 0; // Telemetry @@ -1628,7 +1780,7 @@ class Dialogs { this.windowsMainService.open({ context: OpenContext.DIALOG, cli: this.environmentService.args, - pathsToOpen: paths, + urisToOpen: paths, forceNewWindow: options.forceNewWindow, forceOpenWorkspaceAsFile: options.dialogOptions && !equals(options.dialogOptions.filters, WORKSPACE_FILTER) }); @@ -1636,7 +1788,7 @@ class Dialogs { }); } - private getFileOrFolderPaths(options: IInternalNativeOpenDialogOptions): TPromise { + private getFileOrFolderUris(options: IInternalNativeOpenDialogOptions): TPromise { // Ensure dialog options if (!options.dialogOptions) { @@ -1674,7 +1826,7 @@ class Dialogs { // Remember path in storage for next time this.stateService.setItem(Dialogs.workingDirPickerStorageKey, dirname(paths[0])); - return paths; + return paths.map(path => URI.file(path)); } return void 0; @@ -1695,7 +1847,7 @@ class Dialogs { return windowDialogQueue; } - public showMessageBox(options: Electron.MessageBoxOptions, window?: ICodeWindow): TPromise { + showMessageBox(options: Electron.MessageBoxOptions, window?: ICodeWindow): TPromise { return this.getDialogQueue(window).queue(() => { return new TPromise((c, e) => { dialog.showMessageBox(window ? window.win : void 0, options, (response: number, checkboxChecked: boolean) => { @@ -1705,7 +1857,7 @@ class Dialogs { }); } - public showSaveDialog(options: Electron.SaveDialogOptions, window?: ICodeWindow): TPromise { + showSaveDialog(options: Electron.SaveDialogOptions, window?: ICodeWindow): TPromise { function normalizePath(path: string): string { if (path && isMacintosh) { @@ -1724,7 +1876,7 @@ class Dialogs { }); } - public showOpenDialog(options: Electron.OpenDialogOptions, window?: ICodeWindow): TPromise { + showOpenDialog(options: Electron.OpenDialogOptions, window?: ICodeWindow): TPromise { function normalizePaths(paths: string[]): string[] { if (paths && paths.length > 0 && isMacintosh) { @@ -1768,7 +1920,7 @@ class WorkspacesManager { ) { } - public saveAndEnterWorkspace(window: ICodeWindow, path: string): TPromise { + saveAndEnterWorkspace(window: ICodeWindow, path: string): TPromise { if (!window || !window.win || window.readyState !== ReadyState.READY || !window.openedWorkspace || !path || !this.isValidTargetWorkspacePath(window, path)) { return TPromise.as(null); // return early if the window is not ready or disposed or does not have a workspace } @@ -1776,7 +1928,24 @@ class WorkspacesManager { return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); } - public createAndEnterWorkspace(window: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { + enterWorkspace(window: ICodeWindow, path: string): TPromise { + if (!window || !window.win || window.readyState !== ReadyState.READY) { + return TPromise.as(null); // return early if the window is not ready or disposed + } + + return this.isValidTargetWorkspacePath(window, path).then(isValid => { + if (!isValid) { + return TPromise.as(null); // return early if the workspace is not valid + } + + return this.workspacesMainService.resolveWorkspace(path).then(workspace => { + return this.doOpenWorkspace(window, workspace); + }); + }); + + } + + createAndEnterWorkspace(window: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { if (!window || !window.win || window.readyState !== ReadyState.READY) { return TPromise.as(null); // return early if the window is not ready or disposed } @@ -1827,25 +1996,27 @@ class WorkspacesManager { savePromise = TPromise.as(workspace); } - return savePromise.then(workspace => { - window.focus(); - - // Register window for backups and migrate current backups over - let backupPath: string; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); - } - - // Update window configuration properly based on transition to workspace - window.config.folderPath = void 0; - window.config.workspace = workspace; - window.config.backupPath = backupPath; - - return { workspace, backupPath }; - }); + return savePromise.then(workspace => this.doOpenWorkspace(window, workspace)); } - public pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { + private doOpenWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult { + window.focus(); + + // Register window for backups and migrate current backups over + let backupPath: string; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath); + } + + // Update window configuration properly based on transition to workspace + window.config.folderUri = void 0; + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; + } + + pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { const window = this.windowsMainService.getWindowById(options.windowId) || this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); this.windowsMainService.pickFileAndOpen({ @@ -1863,7 +2034,7 @@ class WorkspacesManager { }); } - public promptToSaveUntitledWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): TPromise { + promptToSaveUntitledWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): TPromise { enum ConfirmResult { SAVE, DONT_SAVE, @@ -1931,7 +2102,7 @@ class WorkspacesManager { private getUntitledWorkspaceSaveDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { if (workspace) { if (isSingleFolderWorkspaceIdentifier(workspace)) { - return dirname(workspace); + return workspace.scheme === Schemas.file ? dirname(workspace.fsPath) : void 0; } const resolvedWorkspace = this.workspacesMainService.resolveWorkspaceSync(workspace.configPath); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 0c5a6074797..94b66896ed0 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,7 +14,7 @@ import * as paths from 'path'; import * as os from 'os'; import * as fs from 'fs'; import { whenDeleted } from 'vs/base/node/pfs'; -import { findFreePort } from 'vs/base/node/ports'; +import { findFreePort, randomPort } from 'vs/base/node/ports'; import { resolveTerminalEncoding } from 'vs/base/node/encoding'; import * as iconv from 'iconv-lite'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; @@ -31,7 +31,7 @@ interface IMainCli { main: (argv: ParsedArgs) => TPromise; } -export async function main(argv: string[]): TPromise { +export async function main(argv: string[]): Promise { let args: ParsedArgs; try { @@ -87,18 +87,17 @@ export async function main(argv: string[]): TPromise { // Write source to target const data = fs.readFileSync(source); - try { + if (isWindows) { + // On Windows we use a different strategy of saving the file + // by first truncating the file and then writing with r+ mode. + // This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) and + // prevent removing alternate data streams + // (see https://github.com/Microsoft/vscode/issues/6363) + fs.truncateSync(target, 0); + writeFileAndFlushSync(target, data, { flag: 'r+' }); + } else { writeFileAndFlushSync(target, data); - } catch (error) { - // On Windows and if the file exists with an EPERM error, we try a different strategy of saving the file - // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows - // (see https://github.com/Microsoft/vscode/issues/931) - if (isWindows && error.code === 'EPERM') { - fs.truncateSync(target, 0); - writeFileAndFlushSync(target, data, { flag: 'r+' }); - } else { - throw error; - } } // Restore previous mode as needed @@ -172,7 +171,7 @@ export async function main(argv: string[]): TPromise { if (!stdinFileError) { // Pipe into tmp file using terminals encoding - resolveTerminalEncoding(verbose).done(encoding => { + resolveTerminalEncoding(verbose).then(encoding => { const converterStream = iconv.decodeStream(encoding); process.stdin.pipe(converterStream).pipe(stdinFileStream); }); @@ -252,13 +251,13 @@ export async function main(argv: string[]): TPromise { // to get better profile traces. Last, we listen on stdout for a signal that tells us to // stop profiling. if (args['prof-startup']) { - const portMain = await findFreePort(9222, 10, 6000); - const portRenderer = await findFreePort(portMain + 1, 10, 6000); - const portExthost = await findFreePort(portRenderer + 1, 10, 6000); + const portMain = await findFreePort(randomPort(), 10, 3000); + const portRenderer = await findFreePort(portMain + 1, 10, 3000); + const portExthost = await findFreePort(portRenderer + 1, 10, 3000); - if (!portMain || !portRenderer || !portExthost) { - console.error('Failed to find free ports for profiler to connect to do.'); - return; + // fail the operation when one of the ports couldn't be accquired. + if (portMain * portRenderer * portExthost === 0) { + throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } const filenamePrefix = paths.join(os.homedir(), Math.random().toString(16).slice(-4)); @@ -271,38 +270,45 @@ export async function main(argv: string[]): TPromise { fs.writeFileSync(filenamePrefix, argv.slice(-6).join('|')); - processCallbacks.push(async child => { + processCallbacks.push(async _child => { + try { + // load and start profiler + const profiler = await import('v8-inspect-profiler'); + const main = await profiler.startProfiling({ port: portMain }); + const renderer = await profiler.startProfiling({ port: portRenderer, tries: 200 }); + const extHost = await profiler.startProfiling({ port: portExthost, tries: 300 }); - // load and start profiler - const profiler = await import('v8-inspect-profiler'); - const main = await profiler.startProfiling({ port: portMain }); - const renderer = await profiler.startProfiling({ port: portRenderer, tries: 200 }); - const extHost = await profiler.startProfiling({ port: portExthost, tries: 300 }); + // wait for the renderer to delete the + // marker file + await whenDeleted(filenamePrefix); - // wait for the renderer to delete the - // marker file - whenDeleted(filenamePrefix); + let profileMain = await main.stop(); + let profileRenderer = await renderer.stop(); + let profileExtHost = await extHost.stop(); + let suffix = ''; - let profileMain = await main.stop(); - let profileRenderer = await renderer.stop(); - let profileExtHost = await extHost.stop(); - let suffix = ''; + if (!process.env['VSCODE_DEV']) { + // when running from a not-development-build we remove + // absolute filenames because we don't want to reveal anything + // about users. We also append the `.txt` suffix to make it + // easier to attach these files to GH issues + profileMain = profiler.rewriteAbsolutePaths(profileMain, 'piiRemoved'); + profileRenderer = profiler.rewriteAbsolutePaths(profileRenderer, 'piiRemoved'); + profileExtHost = profiler.rewriteAbsolutePaths(profileExtHost, 'piiRemoved'); + suffix = '.txt'; + } - if (!process.env['VSCODE_DEV']) { - // when running from a not-development-build we remove - // absolute filenames because we don't want to reveal anything - // about users. We also append the `.txt` suffix to make it - // easier to attach these files to GH issues - profileMain = profiler.rewriteAbsolutePaths(profileMain, 'piiRemoved'); - profileRenderer = profiler.rewriteAbsolutePaths(profileRenderer, 'piiRemoved'); - profileExtHost = profiler.rewriteAbsolutePaths(profileExtHost, 'piiRemoved'); - suffix = '.txt'; + // finally stop profiling and save profiles to disk + await profiler.writeProfile(profileMain, `${filenamePrefix}-main.cpuprofile${suffix}`); + await profiler.writeProfile(profileRenderer, `${filenamePrefix}-renderer.cpuprofile${suffix}`); + await profiler.writeProfile(profileExtHost, `${filenamePrefix}-exthost.cpuprofile${suffix}`); + + // re-create the marker file to signal that profiling is done + fs.writeFileSync(filenamePrefix, ''); + + } catch (e) { + console.error('Failed to profile startup. Make sure to quit Code first.'); } - - // finally stop profiling and save profiles to disk - await profiler.writeProfile(profileMain, `${filenamePrefix}-main.cpuprofile${suffix}`); - await profiler.writeProfile(profileRenderer, `${filenamePrefix}-renderer.cpuprofile${suffix}`); - await profiler.writeProfile(profileExtHost, `${filenamePrefix}-exthost.cpuprofile${suffix}`); }); } @@ -318,7 +324,7 @@ export async function main(argv: string[]): TPromise { env }; - if (typeof args['upload-logs'] !== undefined) { + if (typeof args['upload-logs'] !== 'undefined') { options['stdio'] = ['pipe', 'pipe', 'pipe']; } else if (!verbose) { options['stdio'] = 'ignore'; @@ -333,7 +339,7 @@ export async function main(argv: string[]): TPromise { child.once('exit', () => c(null)); // Complete when wait marker file is deleted - whenDeleted(waitMarkerFilePath).done(c, c); + whenDeleted(waitMarkerFilePath).then(c, c); }).then(() => { // Make sure to delete the tmp stdin file if we have any diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 92f05cb628f..8cce3816254 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -7,6 +7,7 @@ import { localize } from 'vs/nls'; import product from 'vs/platform/node/product'; import pkg from 'vs/platform/node/package'; import * as path from 'path'; +import * as semver from 'semver'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; @@ -38,6 +39,9 @@ import { ILogService, getLogLevel } from 'vs/platform/log/common/log'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CommandLineDialogService } from 'vs/platform/dialogs/node/dialogService'; +import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); @@ -58,26 +62,28 @@ class Main { constructor( @IEnvironmentService private environmentService: IEnvironmentService, @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService + @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService, + @IDialogService private dialogService: IDialogService ) { } run(argv: ParsedArgs): TPromise { // TODO@joao - make this contributable + let returnPromise: TPromise; if (argv['install-source']) { - return this.setInstallSource(argv['install-source']); + returnPromise = this.setInstallSource(argv['install-source']); } else if (argv['list-extensions']) { - return this.listExtensions(argv['show-versions']); + returnPromise = this.listExtensions(argv['show-versions']); } else if (argv['install-extension']) { const arg = argv['install-extension']; const args: string[] = typeof arg === 'string' ? [arg] : arg; - return this.installExtension(args); + returnPromise = this.installExtension(args); } else if (argv['uninstall-extension']) { const arg = argv['uninstall-extension']; const ids: string[] = typeof arg === 'string' ? [arg] : arg; - return this.uninstallExtension(ids); + returnPromise = this.uninstallExtension(ids); } - return undefined; + return returnPromise || TPromise.as(null); } private setInstallSource(installSource: string): TPromise { @@ -96,7 +102,7 @@ class Main { .map(id => () => { const extension = path.isAbsolute(id) ? id : path.join(process.cwd(), id); - return this.extensionManagementService.install(extension).then(() => { + return this.extensionManagementService.install(URI.file(extension)).then(() => { console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed!", getBaseLabel(extension))); }, error => { if (isPromiseCanceledError(error)) { @@ -111,15 +117,8 @@ class Main { const galleryTasks: Task[] = extensions .filter(e => !/\.vsix$/i.test(e)) .map(id => () => { - return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(installed => { - const isInstalled = installed.some(e => getId(e.manifest) === id); - - if (isInstalled) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", id)); - return TPromise.as(null); - } - - return this.extensionGalleryService.query({ names: [id], source: 'cli' }) + return this.extensionManagementService.getInstalled(LocalExtensionType.User) + .then(installed => this.extensionGalleryService.query({ names: [id], source: 'cli' }) .then>(null, err => { if (err.responseText) { try { @@ -129,7 +128,6 @@ class Main { // noop } } - return TPromise.wrapError(err); }) .then(result => { @@ -139,29 +137,52 @@ class Main { return TPromise.wrapError(new Error(`${notFound(id)}\n${useId}`)); } - console.log(localize('foundExtension', "Found '{0}' in the marketplace.", id)); - console.log(localize('installing', "Installing...")); + const [installedExtension] = installed.filter(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(e) }, { id })); + if (installedExtension) { + const outdated = semver.gt(extension.version, installedExtension.manifest.version); + if (outdated) { + const updateMessage = localize('updateMessage', "Extension '{0}' v{1} is already installed, but a newer version {2} is available in the marketplace. Would you like to update?", id, installedExtension.manifest.version, extension.version); + return this.dialogService.show(Severity.Info, updateMessage, [localize('yes', "Yes"), localize('no', "No")]) + .then(option => { + if (option === 0) { + return this.installFromGallery(id, extension); + } + console.log(localize('cancelInstall', "Cancelled installing Extension '{0}'.", id)); + return TPromise.as(null); + }); - return this.extensionManagementService.installFromGallery(extension) - .then( - () => console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed!", id, extension.version)), - error => { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing Extension '{0}'.", id)); - return null; - } else { - return TPromise.wrapError(error); - } - }); - }); - }); + } else { + console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", id)); + return TPromise.as(null); + } + } else { + console.log(localize('foundExtension', "Found '{0}' in the marketplace.", id)); + return this.installFromGallery(id, extension); + } + + })); }); return sequence([...vsixTasks, ...galleryTasks]); } + private installFromGallery(id: string, extension: IGalleryExtension): TPromise { + console.log(localize('installing', "Installing...")); + return this.extensionManagementService.installFromGallery(extension) + .then( + () => console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed!", id, extension.version)), + error => { + if (isPromiseCanceledError(error)) { + console.log(localize('cancelVsixInstall', "Cancelled installing Extension '{0}'.", id)); + return null; + } else { + return TPromise.wrapError(error); + } + }); + } + private uninstallExtension(extensions: string[]): TPromise { - async function getExtensionId(extensionDescription: string): TPromise { + async function getExtensionId(extensionDescription: string): Promise { if (!/\.vsix$/i.test(extensionDescription)) { return extensionDescription; } @@ -174,7 +195,7 @@ class Main { return sequence(extensions.map(extension => () => { return getExtensionId(extension).then(id => { return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(installed => { - const [extension] = installed.filter(e => getId(e.manifest) === id); + const [extension] = installed.filter(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(e) }, { id })); if (!extension) { return TPromise.wrapError(new Error(`${notInstalled(id)}\n${useId}`)); @@ -212,7 +233,7 @@ export function main(argv: ParsedArgs): TPromise { const stateService = accessor.get(IStateService); return TPromise.join([envService.appSettingsHome, envService.extensionsPath].map(p => mkdirp(p))).then(() => { - const { appRoot, extensionsPath, extensionDevelopmentPath, isBuilt, installSourcePath } = envService; + const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = envService; const services = new ServiceCollection(); services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); @@ -221,17 +242,13 @@ export function main(argv: ParsedArgs): TPromise { services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(IDialogService, new SyncDescriptor(CommandLineDialogService)); - if (isBuilt && !extensionDevelopmentPath && !envService.args['disable-telemetry'] && product.enableTelemetry) { - const appenders: AppInsightsAppender[] = []; + const appenders: AppInsightsAppender[] = []; + if (isBuilt && !extensionDevelopmentLocationURI && !envService.args['disable-telemetry'] && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey, logService)); } - // It is important to dispose the AI adapter properly because - // only then they flush remaining data. - process.once('exit', () => appenders.forEach(a => a.dispose())); - const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), commonProperties: resolveCommonProperties(product.commit, pkg.version, stateService.getItem('telemetry.machineId'), installSourcePath), @@ -246,7 +263,10 @@ export function main(argv: ParsedArgs): TPromise { const instantiationService2 = instantiationService.createChild(services); const main = instantiationService2.createInstance(Main); - return main.run(argv); + return main.run(argv).then(() => { + // Dispose the AI adapter so that remaining data gets flushed. + return combinedAppender(...appenders).dispose(); + }); }); }); } diff --git a/src/vs/code/node/paths.ts b/src/vs/code/node/paths.ts index d54353ff72e..520bd6a7b45 100644 --- a/src/vs/code/node/paths.ts +++ b/src/vs/code/node/paths.ts @@ -12,16 +12,17 @@ import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import * as types from 'vs/base/common/types'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { realpathSync } from 'vs/base/node/extfs'; +import { sanitizeFilePath } from 'vs/base/node/extfs'; export function validatePaths(args: ParsedArgs): ParsedArgs { + // Track URLs if they're going to be used if (args['open-url']) { args._urls = args._; args._ = []; } - // Realpath/normalize paths and watch out for goto line mode + // Normalize paths and watch out for goto line mode const paths = doValidatePaths(args._, args.goto); // Update environment @@ -46,26 +47,20 @@ function doValidatePaths(args: string[], gotoLineMode?: boolean): string[] { pathCandidate = preparePath(cwd, pathCandidate); } - let realPath: string; - try { - realPath = realpathSync(pathCandidate); - } catch (error) { - // in case of an error, assume the user wants to create this file - // if the path is relative, we join it to the cwd - realPath = path.normalize(path.isAbsolute(pathCandidate) ? pathCandidate : path.join(cwd, pathCandidate)); - } + const sanitizedFilePath = sanitizeFilePath(pathCandidate, cwd); - const basename = path.basename(realPath); + const basename = path.basename(sanitizedFilePath); if (basename /* can be empty if code is opened on root */ && !paths.isValidBasename(basename)) { return null; // do not allow invalid file names } if (gotoLineMode) { - parsedPath.path = realPath; + parsedPath.path = sanitizedFilePath; + return toPath(parsedPath); } - return realPath; + return sanitizedFilePath; }); const caseInsensitive = platform.isWindows || platform.isMacintosh; diff --git a/src/vs/code/node/windowsFinder.ts b/src/vs/code/node/windowsFinder.ts index 61d9427fa69..2834f81e51a 100644 --- a/src/vs/code/node/windowsFinder.ts +++ b/src/vs/code/node/windowsFinder.ts @@ -8,13 +8,14 @@ import * as platform from 'vs/base/common/platform'; import * as paths from 'vs/base/common/paths'; import { OpenContext } from 'vs/platform/windows/common/windows'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IResolvedWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; export interface ISimpleWindow { openedWorkspace?: IWorkspaceIdentifier; - openedFolderPath?: string; - openedFilePath?: string; + openedFolderUri?: URI; + openedFileUri?: URI; extensionDevelopmentPath?: string; lastFocusTime: number; } @@ -24,15 +25,15 @@ export interface IBestWindowOrFolderOptions { newWindow: boolean; reuseWindow: boolean; context: OpenContext; - filePath?: string; + fileUri?: URI; userHome?: string; codeSettingsFolder?: string; workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace; } -export function findBestWindowOrFolderForFile({ windows, newWindow, reuseWindow, context, filePath, userHome, codeSettingsFolder, workspaceResolver }: IBestWindowOrFolderOptions): W | string { - if (!newWindow && filePath && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { - const windowOnFilePath = findWindowOnFilePath(windows, filePath, workspaceResolver); +export function findBestWindowOrFolderForFile({ windows, newWindow, reuseWindow, context, fileUri, workspaceResolver }: IBestWindowOrFolderOptions): W { + if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { + const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); if (windowOnFilePath) { return windowOnFilePath; } @@ -41,22 +42,22 @@ export function findBestWindowOrFolderForFile({ windows return !newWindow ? getLastActiveWindow(windows) : null; } -function findWindowOnFilePath(windows: W[], filePath: string, workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace): W { +function findWindowOnFilePath(windows: W[], fileUri: URI, workspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace): W { // First check for windows with workspaces that have a parent folder of the provided path opened const workspaceWindows = windows.filter(window => !!window.openedWorkspace); for (let i = 0; i < workspaceWindows.length; i++) { const window = workspaceWindows[i]; const resolvedWorkspace = workspaceResolver(window.openedWorkspace); - if (resolvedWorkspace && resolvedWorkspace.folders.some(folder => folder.uri.scheme === Schemas.file && paths.isEqualOrParent(filePath, folder.uri.fsPath, !platform.isLinux /* ignorecase */))) { + if (resolvedWorkspace && resolvedWorkspace.folders.some(folder => isEqualOrParent(fileUri, folder.uri))) { return window; } } // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => typeof window.openedFolderPath === 'string' && paths.isEqualOrParent(filePath, window.openedFolderPath, !platform.isLinux /* ignorecase */)); + const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && isEqualOrParent(fileUri, window.openedFolderUri)); if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderPath.length - b.openedFolderPath.length))[0]; + return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri.path.length - b.openedFolderUri.path.length))[0]; } return null; @@ -69,51 +70,50 @@ export function getLastActiveWindow(windows: W[]): W { } export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W { - return windows.filter(window => { - - // match on folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (typeof window.openedFolderPath === 'string' && (paths.isEqual(window.openedFolderPath, workspace, !platform.isLinux /* ignorecase */))) { - return true; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + for (const window of windows) { + // match on folder + if (isSingleFolderWorkspaceIdentifier(workspace)) { + if (window.openedFolderUri && isEqual(window.openedFolderUri, workspace)) { + return window; + } } } - - // match on workspace - else { + } else if (isWorkspaceIdentifier(workspace)) { + for (const window of windows) { + // match on workspace if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { - return true; + return window; } } - - return false; - })[0]; + } + return null; } export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPath: string): W { - return windows.filter(window => { - - // match on extension development path + for (const window of windows) { + // match on extension development path. The path can be a path or uri string, using paths.isEqual is not 100% correct but good enough if (paths.isEqual(window.extensionDevelopmentPath, extensionDevelopmentPath, !platform.isLinux /* ignorecase */)) { - return true; + return window; } - - return false; - })[0]; + } + return null; } -export function findWindowOnWorkspaceOrFolderPath(windows: W[], path: string): W { - return windows.filter(window => { - +export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI): W { + if (!uri) { + return null; + } + for (const window of windows) { // check for workspace config path - if (window.openedWorkspace && paths.isEqual(window.openedWorkspace.configPath, path, !platform.isLinux /* ignorecase */)) { - return true; + if (window.openedWorkspace && isEqual(URI.file(window.openedWorkspace.configPath), uri, !platform.isLinux /* ignorecase */)) { + return window; } // check for folder path - if (window.openedFolderPath && paths.isEqual(window.openedFolderPath, path, !platform.isLinux /* ignorecase */)) { - return true; + if (window.openedFolderUri && isEqual(window.openedFolderUri, uri)) { + return window; } - - return false; - })[0]; + } + return null; } diff --git a/src/vs/code/test/node/windowsFinder.test.ts b/src/vs/code/test/node/windowsFinder.test.ts index 084d8546224..05f0a8ecb24 100644 --- a/src/vs/code/test/node/windowsFinder.test.ts +++ b/src/vs/code/test/node/windowsFinder.test.ts @@ -10,8 +10,10 @@ import { findBestWindowOrFolderForFile, ISimpleWindow, IBestWindowOrFolderOption import { OpenContext } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { URI } from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; -const fixturesFolder = require.toUrl('./fixtures'); +const fixturesFolder = getPathFromAmdModule(require, './fixtures'); const testWorkspace: IWorkspaceIdentifier = { id: Date.now().toString(), @@ -30,10 +32,10 @@ function options(custom?: Partial>): I }; } -const vscodeFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder') }; -const lastActiveWindow = { lastFocusTime: 3, openedFolderPath: null }; -const noVscodeFolderWindow = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; -const windows = [ +const vscodeFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; +const lastActiveWindow: ISimpleWindow = { lastFocusTime: 3, openedFolderUri: null }; +const noVscodeFolderWindow: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; +const windows: ISimpleWindow[] = [ vscodeFolderWindow, lastActiveWindow, noVscodeFolderWindow, @@ -44,32 +46,32 @@ suite('WindowsFinder', () => { test('New window without folder when no windows exist', () => { assert.equal(findBestWindowOrFolderForFile(options()), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'no_vscode_folder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) })), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), newWindow: true })), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), reuseWindow: true })), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), context: OpenContext.API })), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) })), null); assert.equal(findBestWindowOrFolderForFile(options({ - filePath: path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt')) })), null); }); test('New window without folder when windows exist', () => { assert.equal(findBestWindowOrFolderForFile(options({ windows, - filePath: path.join(fixturesFolder, 'no_vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), newWindow: true })), null); }); @@ -80,16 +82,16 @@ suite('WindowsFinder', () => { })), lastActiveWindow); assert.equal(findBestWindowOrFolderForFile(options({ windows, - filePath: path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt')) })), lastActiveWindow); assert.equal(findBestWindowOrFolderForFile(options({ windows: [lastActiveWindow, noVscodeFolderWindow], - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), reuseWindow: true })), lastActiveWindow); assert.equal(findBestWindowOrFolderForFile(options({ windows, - filePath: path.join(fixturesFolder, 'no_vscode_folder', 'file.txt'), + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), context: OpenContext.API })), lastActiveWindow); }); @@ -97,33 +99,33 @@ suite('WindowsFinder', () => { test('Existing window with folder', () => { assert.equal(findBestWindowOrFolderForFile(options({ windows, - filePath: path.join(fixturesFolder, 'no_vscode_folder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) })), noVscodeFolderWindow); assert.equal(findBestWindowOrFolderForFile(options({ windows, - filePath: path.join(fixturesFolder, 'vscode_folder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) })), vscodeFolderWindow); - const window = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder') }; + const window: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], - filePath: path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) })), window); }); test('More specific existing window wins', () => { - const window = { lastFocusTime: 2, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder') }; - const nestedFolderWindow = { lastFocusTime: 1, openedFolderPath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder') }; + const window: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; + const nestedFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window, nestedFolderWindow], - filePath: path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) })), nestedFolderWindow); }); test('Workspace folder wins', () => { - const window = { lastFocusTime: 1, openedWorkspace: testWorkspace }; + const window: ISimpleWindow = { lastFocusTime: 1, openedWorkspace: testWorkspace }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], - filePath: path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt') + fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')) })), window); }); }); diff --git a/src/vs/css.build.js b/src/vs/css.build.js index 7f34c9b158a..4a617c57ed8 100644 --- a/src/vs/css.build.js +++ b/src/vs/css.build.js @@ -17,346 +17,346 @@ var _cssPluginGlobal = this; var CSSBuildLoaderPlugin; (function (CSSBuildLoaderPlugin) { - var global = _cssPluginGlobal || {}; - /** - * Known issue: - * - In IE there is no way to know if the CSS file loaded successfully or not. - */ - var BrowserCSSLoader = (function () { - function BrowserCSSLoader() { - this._pendingLoads = 0; - } - BrowserCSSLoader.prototype.attachListeners = function (name, linkNode, callback, errorback) { - var unbind = function () { - linkNode.removeEventListener('load', loadEventListener); - linkNode.removeEventListener('error', errorEventListener); - }; - var loadEventListener = function (e) { - unbind(); - callback(); - }; - var errorEventListener = function (e) { - unbind(); - errorback(e); - }; - linkNode.addEventListener('load', loadEventListener); - linkNode.addEventListener('error', errorEventListener); - }; - BrowserCSSLoader.prototype._onLoad = function (name, callback) { - this._pendingLoads--; - callback(); - }; - BrowserCSSLoader.prototype._onLoadError = function (name, errorback, err) { - this._pendingLoads--; - errorback(err); - }; - BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) { - this._pendingLoads++; - var head = document.head || document.getElementsByTagName('head')[0]; - var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script'); - if (other.length > 0) { - head.insertBefore(linkNode, other[other.length - 1]); - } - else { - head.appendChild(linkNode); - } - }; - BrowserCSSLoader.prototype.createLinkTag = function (name, cssUrl, externalCallback, externalErrorback) { - var _this = this; - var linkNode = document.createElement('link'); - linkNode.setAttribute('rel', 'stylesheet'); - linkNode.setAttribute('type', 'text/css'); - linkNode.setAttribute('data-name', name); - var callback = function () { return _this._onLoad(name, externalCallback); }; - var errorback = function (err) { return _this._onLoadError(name, externalErrorback, err); }; - this.attachListeners(name, linkNode, callback, errorback); - linkNode.setAttribute('href', cssUrl); - return linkNode; - }; - BrowserCSSLoader.prototype._linkTagExists = function (name, cssUrl) { - var i, len, nameAttr, hrefAttr, links = document.getElementsByTagName('link'); - for (i = 0, len = links.length; i < len; i++) { - nameAttr = links[i].getAttribute('data-name'); - hrefAttr = links[i].getAttribute('href'); - if (nameAttr === name || hrefAttr === cssUrl) { - return true; - } - } - return false; - }; - BrowserCSSLoader.prototype.load = function (name, cssUrl, externalCallback, externalErrorback) { - if (this._linkTagExists(name, cssUrl)) { - externalCallback(); - return; - } - var linkNode = this.createLinkTag(name, cssUrl, externalCallback, externalErrorback); - this._insertLinkNode(linkNode); - }; - return BrowserCSSLoader; - }()); - var NodeCSSLoader = (function () { - function NodeCSSLoader() { - this.fs = require.nodeRequire('fs'); - } - NodeCSSLoader.prototype.load = function (name, cssUrl, externalCallback, externalErrorback) { - var contents = this.fs.readFileSync(cssUrl, 'utf8'); - // Remove BOM - if (contents.charCodeAt(0) === NodeCSSLoader.BOM_CHAR_CODE) { - contents = contents.substring(1); - } - externalCallback(contents); - }; - return NodeCSSLoader; - }()); - NodeCSSLoader.BOM_CHAR_CODE = 65279; - // ------------------------------ Finally, the plugin - var CSSPlugin = (function () { - function CSSPlugin(cssLoader) { - this.cssLoader = cssLoader; - } - CSSPlugin.prototype.load = function (name, req, load, config) { - config = config || {}; - var myConfig = config['vs/css'] || {}; - global.inlineResources = myConfig.inlineResources; - global.inlineResourcesLimit = myConfig.inlineResourcesLimit || 5000; - var cssUrl = req.toUrl(name + '.css'); - this.cssLoader.load(name, cssUrl, function (contents) { - // Contents has the CSS file contents if we are in a build - if (config.isBuild) { - CSSPlugin.BUILD_MAP[name] = contents; - CSSPlugin.BUILD_PATH_MAP[name] = cssUrl; - } - load({}); - }, function (err) { - if (typeof load.error === 'function') { - load.error('Could not find ' + cssUrl + ' or it was empty'); - } - }); - }; - CSSPlugin.prototype.write = function (pluginName, moduleName, write) { - // getEntryPoint is a Monaco extension to r.js - var entryPoint = write.getEntryPoint(); - // r.js destroys the context of this plugin between calling 'write' and 'writeFile' - // so the only option at this point is to leak the data to a global - global.cssPluginEntryPoints = global.cssPluginEntryPoints || {}; - global.cssPluginEntryPoints[entryPoint] = global.cssPluginEntryPoints[entryPoint] || []; - global.cssPluginEntryPoints[entryPoint].push({ - moduleName: moduleName, - contents: CSSPlugin.BUILD_MAP[moduleName], - fsPath: CSSPlugin.BUILD_PATH_MAP[moduleName], - }); - write.asModule(pluginName + '!' + moduleName, 'define([\'vs/css!' + entryPoint + '\'], {});'); - }; - CSSPlugin.prototype.writeFile = function (pluginName, moduleName, req, write, config) { - if (global.cssPluginEntryPoints && global.cssPluginEntryPoints.hasOwnProperty(moduleName)) { - var fileName = req.toUrl(moduleName + '.css'); - var contents = [ - '/*---------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' *--------------------------------------------------------*/' - ], entries = global.cssPluginEntryPoints[moduleName]; - for (var i = 0; i < entries.length; i++) { - if (global.inlineResources) { - contents.push(Utilities.rewriteOrInlineUrls(entries[i].fsPath, entries[i].moduleName, moduleName, entries[i].contents, global.inlineResources === 'base64', global.inlineResourcesLimit)); - } - else { - contents.push(Utilities.rewriteUrls(entries[i].moduleName, moduleName, entries[i].contents)); - } - } - write(fileName, contents.join('\r\n')); - } - }; - CSSPlugin.prototype.getInlinedResources = function () { - return global.cssInlinedResources || []; - }; - return CSSPlugin; - }()); - CSSPlugin.BUILD_MAP = {}; - CSSPlugin.BUILD_PATH_MAP = {}; - CSSBuildLoaderPlugin.CSSPlugin = CSSPlugin; - var Utilities = (function () { - function Utilities() { - } - Utilities.startsWith = function (haystack, needle) { - return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; - }; - /** - * Find the path of a file. - */ - Utilities.pathOf = function (filename) { - var lastSlash = filename.lastIndexOf('/'); - if (lastSlash !== -1) { - return filename.substr(0, lastSlash + 1); - } - else { - return ''; - } - }; - /** - * A conceptual a + b for paths. - * Takes into account if `a` contains a protocol. - * Also normalizes the result: e.g.: a/b/ + ../c => a/c - */ - Utilities.joinPaths = function (a, b) { - function findSlashIndexAfterPrefix(haystack, prefix) { - if (Utilities.startsWith(haystack, prefix)) { - return Math.max(prefix.length, haystack.indexOf('/', prefix.length)); - } - return 0; - } - var aPathStartIndex = 0; - aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, '//'); - aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, 'http://'); - aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, 'https://'); - function pushPiece(pieces, piece) { - if (piece === './') { - // Ignore - return; - } - if (piece === '../') { - var prevPiece = (pieces.length > 0 ? pieces[pieces.length - 1] : null); - if (prevPiece && prevPiece === '/') { - // Ignore - return; - } - if (prevPiece && prevPiece !== '../') { - // Pop - pieces.pop(); - return; - } - } - // Push - pieces.push(piece); - } - function push(pieces, path) { - while (path.length > 0) { - var slashIndex = path.indexOf('/'); - var piece = (slashIndex >= 0 ? path.substring(0, slashIndex + 1) : path); - path = (slashIndex >= 0 ? path.substring(slashIndex + 1) : ''); - pushPiece(pieces, piece); - } - } - var pieces = []; - push(pieces, a.substr(aPathStartIndex)); - if (b.length > 0 && b.charAt(0) === '/') { - pieces = []; - } - push(pieces, b); - return a.substring(0, aPathStartIndex) + pieces.join(''); - }; - Utilities.commonPrefix = function (str1, str2) { - var len = Math.min(str1.length, str2.length); - for (var i = 0; i < len; i++) { - if (str1.charCodeAt(i) !== str2.charCodeAt(i)) { - break; - } - } - return str1.substring(0, i); - }; - Utilities.commonFolderPrefix = function (fromPath, toPath) { - var prefix = Utilities.commonPrefix(fromPath, toPath); - var slashIndex = prefix.lastIndexOf('/'); - if (slashIndex === -1) { - return ''; - } - return prefix.substring(0, slashIndex + 1); - }; - Utilities.relativePath = function (fromPath, toPath) { - if (Utilities.startsWith(toPath, '/') || Utilities.startsWith(toPath, 'http://') || Utilities.startsWith(toPath, 'https://')) { - return toPath; - } - // Ignore common folder prefix - var prefix = Utilities.commonFolderPrefix(fromPath, toPath); - fromPath = fromPath.substr(prefix.length); - toPath = toPath.substr(prefix.length); - var upCount = fromPath.split('/').length; - var result = ''; - for (var i = 1; i < upCount; i++) { - result += '../'; - } - return result + toPath; - }; - Utilities._replaceURL = function (contents, replacer) { - // Use ")" as the terminator as quotes are oftentimes not used at all - return contents.replace(/url\(\s*([^\)]+)\s*\)?/g, function (_) { - var matches = []; - for (var _i = 1; _i < arguments.length; _i++) { - matches[_i - 1] = arguments[_i]; - } - var url = matches[0]; - // Eliminate starting quotes (the initial whitespace is not captured) - if (url.charAt(0) === '"' || url.charAt(0) === '\'') { - url = url.substring(1); - } - // The ending whitespace is captured - while (url.length > 0 && (url.charAt(url.length - 1) === ' ' || url.charAt(url.length - 1) === '\t')) { - url = url.substring(0, url.length - 1); - } - // Eliminate ending quotes - if (url.charAt(url.length - 1) === '"' || url.charAt(url.length - 1) === '\'') { - url = url.substring(0, url.length - 1); - } - if (!Utilities.startsWith(url, 'data:') && !Utilities.startsWith(url, 'http://') && !Utilities.startsWith(url, 'https://')) { - url = replacer(url); - } - return 'url(' + url + ')'; - }); - }; - Utilities.rewriteUrls = function (originalFile, newFile, contents) { - return this._replaceURL(contents, function (url) { - var absoluteUrl = Utilities.joinPaths(Utilities.pathOf(originalFile), url); - return Utilities.relativePath(newFile, absoluteUrl); - }); - }; - Utilities.rewriteOrInlineUrls = function (originalFileFSPath, originalFile, newFile, contents, forceBase64, inlineByteLimit) { - var fs = require.nodeRequire('fs'); - var path = require.nodeRequire('path'); - return this._replaceURL(contents, function (url) { - if (/\.(svg|png)$/.test(url)) { - var fsPath = path.join(path.dirname(originalFileFSPath), url); - var fileContents = fs.readFileSync(fsPath); - if (fileContents.length < inlineByteLimit) { - global.cssInlinedResources = global.cssInlinedResources || []; - var normalizedFSPath = fsPath.replace(/\\/g, '/'); - if (global.cssInlinedResources.indexOf(normalizedFSPath) >= 0) { - console.warn('CSS INLINING IMAGE AT ' + fsPath + ' MORE THAN ONCE. CONSIDER CONSOLIDATING CSS RULES'); - } - global.cssInlinedResources.push(normalizedFSPath); - var MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; - var DATA = ';base64,' + fileContents.toString('base64'); - if (!forceBase64 && /\.svg$/.test(url)) { - // .svg => url encode as explained at https://codepen.io/tigt/post/optimizing-svgs-in-data-uris - var newText = fileContents.toString() - .replace(/"/g, '\'') - .replace(//g, '%3E') - .replace(/&/g, '%26') - .replace(/#/g, '%23') - .replace(/\s+/g, ' '); - var encodedData = ',' + newText; - if (encodedData.length < DATA.length) { - DATA = encodedData; - } - } - return '"data:' + MIME + DATA + '"'; - } - } - var absoluteUrl = Utilities.joinPaths(Utilities.pathOf(originalFile), url); - return Utilities.relativePath(newFile, absoluteUrl); - }); - }; - return Utilities; - }()); - CSSBuildLoaderPlugin.Utilities = Utilities; - (function () { - var cssLoader = null; - var isElectron = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions['electron'] !== 'undefined'); - if (typeof process !== 'undefined' && process.versions && !!process.versions.node && !isElectron) { - cssLoader = new NodeCSSLoader(); - } - else { - cssLoader = new BrowserCSSLoader(); - } - define('vs/css', new CSSPlugin(cssLoader)); - })(); + var global = _cssPluginGlobal || {}; + /** + * Known issue: + * - In IE there is no way to know if the CSS file loaded successfully or not. + */ + var BrowserCSSLoader = /** @class */ (function () { + function BrowserCSSLoader() { + this._pendingLoads = 0; + } + BrowserCSSLoader.prototype.attachListeners = function (name, linkNode, callback, errorback) { + var unbind = function () { + linkNode.removeEventListener('load', loadEventListener); + linkNode.removeEventListener('error', errorEventListener); + }; + var loadEventListener = function (e) { + unbind(); + callback(); + }; + var errorEventListener = function (e) { + unbind(); + errorback(e); + }; + linkNode.addEventListener('load', loadEventListener); + linkNode.addEventListener('error', errorEventListener); + }; + BrowserCSSLoader.prototype._onLoad = function (name, callback) { + this._pendingLoads--; + callback(); + }; + BrowserCSSLoader.prototype._onLoadError = function (name, errorback, err) { + this._pendingLoads--; + errorback(err); + }; + BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) { + this._pendingLoads++; + var head = document.head || document.getElementsByTagName('head')[0]; + var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script'); + if (other.length > 0) { + head.insertBefore(linkNode, other[other.length - 1]); + } + else { + head.appendChild(linkNode); + } + }; + BrowserCSSLoader.prototype.createLinkTag = function (name, cssUrl, externalCallback, externalErrorback) { + var _this = this; + var linkNode = document.createElement('link'); + linkNode.setAttribute('rel', 'stylesheet'); + linkNode.setAttribute('type', 'text/css'); + linkNode.setAttribute('data-name', name); + var callback = function () { return _this._onLoad(name, externalCallback); }; + var errorback = function (err) { return _this._onLoadError(name, externalErrorback, err); }; + this.attachListeners(name, linkNode, callback, errorback); + linkNode.setAttribute('href', cssUrl); + return linkNode; + }; + BrowserCSSLoader.prototype._linkTagExists = function (name, cssUrl) { + var i, len, nameAttr, hrefAttr, links = document.getElementsByTagName('link'); + for (i = 0, len = links.length; i < len; i++) { + nameAttr = links[i].getAttribute('data-name'); + hrefAttr = links[i].getAttribute('href'); + if (nameAttr === name || hrefAttr === cssUrl) { + return true; + } + } + return false; + }; + BrowserCSSLoader.prototype.load = function (name, cssUrl, externalCallback, externalErrorback) { + if (this._linkTagExists(name, cssUrl)) { + externalCallback(); + return; + } + var linkNode = this.createLinkTag(name, cssUrl, externalCallback, externalErrorback); + this._insertLinkNode(linkNode); + }; + return BrowserCSSLoader; + }()); + var NodeCSSLoader = /** @class */ (function () { + function NodeCSSLoader() { + this.fs = require.nodeRequire('fs'); + } + NodeCSSLoader.prototype.load = function (name, cssUrl, externalCallback, externalErrorback) { + var contents = this.fs.readFileSync(cssUrl, 'utf8'); + // Remove BOM + if (contents.charCodeAt(0) === NodeCSSLoader.BOM_CHAR_CODE) { + contents = contents.substring(1); + } + externalCallback(contents); + }; + NodeCSSLoader.BOM_CHAR_CODE = 65279; + return NodeCSSLoader; + }()); + // ------------------------------ Finally, the plugin + var CSSPlugin = /** @class */ (function () { + function CSSPlugin(cssLoader) { + this.cssLoader = cssLoader; + } + CSSPlugin.prototype.load = function (name, req, load, config) { + config = config || {}; + var myConfig = config['vs/css'] || {}; + global.inlineResources = myConfig.inlineResources; + global.inlineResourcesLimit = myConfig.inlineResourcesLimit || 5000; + var cssUrl = req.toUrl(name + '.css'); + this.cssLoader.load(name, cssUrl, function (contents) { + // Contents has the CSS file contents if we are in a build + if (config.isBuild) { + CSSPlugin.BUILD_MAP[name] = contents; + CSSPlugin.BUILD_PATH_MAP[name] = cssUrl; + } + load({}); + }, function (err) { + if (typeof load.error === 'function') { + load.error('Could not find ' + cssUrl + ' or it was empty'); + } + }); + }; + CSSPlugin.prototype.write = function (pluginName, moduleName, write) { + // getEntryPoint is a Monaco extension to r.js + var entryPoint = write.getEntryPoint(); + // r.js destroys the context of this plugin between calling 'write' and 'writeFile' + // so the only option at this point is to leak the data to a global + global.cssPluginEntryPoints = global.cssPluginEntryPoints || {}; + global.cssPluginEntryPoints[entryPoint] = global.cssPluginEntryPoints[entryPoint] || []; + global.cssPluginEntryPoints[entryPoint].push({ + moduleName: moduleName, + contents: CSSPlugin.BUILD_MAP[moduleName], + fsPath: CSSPlugin.BUILD_PATH_MAP[moduleName], + }); + write.asModule(pluginName + '!' + moduleName, 'define([\'vs/css!' + entryPoint + '\'], {});'); + }; + CSSPlugin.prototype.writeFile = function (pluginName, moduleName, req, write, config) { + if (global.cssPluginEntryPoints && global.cssPluginEntryPoints.hasOwnProperty(moduleName)) { + var fileName = req.toUrl(moduleName + '.css'); + var contents = [ + '/*---------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' *--------------------------------------------------------*/' + ], entries = global.cssPluginEntryPoints[moduleName]; + for (var i = 0; i < entries.length; i++) { + if (global.inlineResources) { + contents.push(Utilities.rewriteOrInlineUrls(entries[i].fsPath, entries[i].moduleName, moduleName, entries[i].contents, global.inlineResources === 'base64', global.inlineResourcesLimit)); + } + else { + contents.push(Utilities.rewriteUrls(entries[i].moduleName, moduleName, entries[i].contents)); + } + } + write(fileName, contents.join('\r\n')); + } + }; + CSSPlugin.prototype.getInlinedResources = function () { + return global.cssInlinedResources || []; + }; + CSSPlugin.BUILD_MAP = {}; + CSSPlugin.BUILD_PATH_MAP = {}; + return CSSPlugin; + }()); + CSSBuildLoaderPlugin.CSSPlugin = CSSPlugin; + var Utilities = /** @class */ (function () { + function Utilities() { + } + Utilities.startsWith = function (haystack, needle) { + return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; + }; + /** + * Find the path of a file. + */ + Utilities.pathOf = function (filename) { + var lastSlash = filename.lastIndexOf('/'); + if (lastSlash !== -1) { + return filename.substr(0, lastSlash + 1); + } + else { + return ''; + } + }; + /** + * A conceptual a + b for paths. + * Takes into account if `a` contains a protocol. + * Also normalizes the result: e.g.: a/b/ + ../c => a/c + */ + Utilities.joinPaths = function (a, b) { + function findSlashIndexAfterPrefix(haystack, prefix) { + if (Utilities.startsWith(haystack, prefix)) { + return Math.max(prefix.length, haystack.indexOf('/', prefix.length)); + } + return 0; + } + var aPathStartIndex = 0; + aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, '//'); + aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, 'http://'); + aPathStartIndex = aPathStartIndex || findSlashIndexAfterPrefix(a, 'https://'); + function pushPiece(pieces, piece) { + if (piece === './') { + // Ignore + return; + } + if (piece === '../') { + var prevPiece = (pieces.length > 0 ? pieces[pieces.length - 1] : null); + if (prevPiece && prevPiece === '/') { + // Ignore + return; + } + if (prevPiece && prevPiece !== '../') { + // Pop + pieces.pop(); + return; + } + } + // Push + pieces.push(piece); + } + function push(pieces, path) { + while (path.length > 0) { + var slashIndex = path.indexOf('/'); + var piece = (slashIndex >= 0 ? path.substring(0, slashIndex + 1) : path); + path = (slashIndex >= 0 ? path.substring(slashIndex + 1) : ''); + pushPiece(pieces, piece); + } + } + var pieces = []; + push(pieces, a.substr(aPathStartIndex)); + if (b.length > 0 && b.charAt(0) === '/') { + pieces = []; + } + push(pieces, b); + return a.substring(0, aPathStartIndex) + pieces.join(''); + }; + Utilities.commonPrefix = function (str1, str2) { + var len = Math.min(str1.length, str2.length); + for (var i = 0; i < len; i++) { + if (str1.charCodeAt(i) !== str2.charCodeAt(i)) { + break; + } + } + return str1.substring(0, i); + }; + Utilities.commonFolderPrefix = function (fromPath, toPath) { + var prefix = Utilities.commonPrefix(fromPath, toPath); + var slashIndex = prefix.lastIndexOf('/'); + if (slashIndex === -1) { + return ''; + } + return prefix.substring(0, slashIndex + 1); + }; + Utilities.relativePath = function (fromPath, toPath) { + if (Utilities.startsWith(toPath, '/') || Utilities.startsWith(toPath, 'http://') || Utilities.startsWith(toPath, 'https://')) { + return toPath; + } + // Ignore common folder prefix + var prefix = Utilities.commonFolderPrefix(fromPath, toPath); + fromPath = fromPath.substr(prefix.length); + toPath = toPath.substr(prefix.length); + var upCount = fromPath.split('/').length; + var result = ''; + for (var i = 1; i < upCount; i++) { + result += '../'; + } + return result + toPath; + }; + Utilities._replaceURL = function (contents, replacer) { + // Use ")" as the terminator as quotes are oftentimes not used at all + return contents.replace(/url\(\s*([^\)]+)\s*\)?/g, function (_) { + var matches = []; + for (var _i = 1; _i < arguments.length; _i++) { + matches[_i - 1] = arguments[_i]; + } + var url = matches[0]; + // Eliminate starting quotes (the initial whitespace is not captured) + if (url.charAt(0) === '"' || url.charAt(0) === '\'') { + url = url.substring(1); + } + // The ending whitespace is captured + while (url.length > 0 && (url.charAt(url.length - 1) === ' ' || url.charAt(url.length - 1) === '\t')) { + url = url.substring(0, url.length - 1); + } + // Eliminate ending quotes + if (url.charAt(url.length - 1) === '"' || url.charAt(url.length - 1) === '\'') { + url = url.substring(0, url.length - 1); + } + if (!Utilities.startsWith(url, 'data:') && !Utilities.startsWith(url, 'http://') && !Utilities.startsWith(url, 'https://')) { + url = replacer(url); + } + return 'url(' + url + ')'; + }); + }; + Utilities.rewriteUrls = function (originalFile, newFile, contents) { + return this._replaceURL(contents, function (url) { + var absoluteUrl = Utilities.joinPaths(Utilities.pathOf(originalFile), url); + return Utilities.relativePath(newFile, absoluteUrl); + }); + }; + Utilities.rewriteOrInlineUrls = function (originalFileFSPath, originalFile, newFile, contents, forceBase64, inlineByteLimit) { + var fs = require.nodeRequire('fs'); + var path = require.nodeRequire('path'); + return this._replaceURL(contents, function (url) { + if (/\.(svg|png)$/.test(url)) { + var fsPath = path.join(path.dirname(originalFileFSPath), url); + var fileContents = fs.readFileSync(fsPath); + if (fileContents.length < inlineByteLimit) { + global.cssInlinedResources = global.cssInlinedResources || []; + var normalizedFSPath = fsPath.replace(/\\/g, '/'); + if (global.cssInlinedResources.indexOf(normalizedFSPath) >= 0) { + console.warn('CSS INLINING IMAGE AT ' + fsPath + ' MORE THAN ONCE. CONSIDER CONSOLIDATING CSS RULES'); + } + global.cssInlinedResources.push(normalizedFSPath); + var MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; + var DATA = ';base64,' + fileContents.toString('base64'); + if (!forceBase64 && /\.svg$/.test(url)) { + // .svg => url encode as explained at https://codepen.io/tigt/post/optimizing-svgs-in-data-uris + var newText = fileContents.toString() + .replace(/"/g, '\'') + .replace(//g, '%3E') + .replace(/&/g, '%26') + .replace(/#/g, '%23') + .replace(/\s+/g, ' '); + var encodedData = ',' + newText; + if (encodedData.length < DATA.length) { + DATA = encodedData; + } + } + return '"data:' + MIME + DATA + '"'; + } + } + var absoluteUrl = Utilities.joinPaths(Utilities.pathOf(originalFile), url); + return Utilities.relativePath(newFile, absoluteUrl); + }); + }; + return Utilities; + }()); + CSSBuildLoaderPlugin.Utilities = Utilities; + (function () { + var cssLoader = null; + var isElectron = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions['electron'] !== 'undefined'); + if (typeof process !== 'undefined' && process.versions && !!process.versions.node && !isElectron) { + cssLoader = new NodeCSSLoader(); + } + else { + cssLoader = new BrowserCSSLoader(); + } + define('vs/css', new CSSPlugin(cssLoader)); + })(); })(CSSBuildLoaderPlugin || (CSSBuildLoaderPlugin = {})); diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 6e5d782a037..63ba2d59a99 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -29,11 +29,7 @@ export class CharWidthRequest { } } -interface ICharWidthReader { - read(): void; -} - -class DomCharWidthReader implements ICharWidthReader { +class DomCharWidthReader { private readonly _bareFontInfo: BareFontInfo; private readonly _requests: CharWidthRequest[]; diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts index 74cb598bddf..f83aefc5125 100644 --- a/src/vs/editor/browser/config/configuration.ts +++ b/src/vs/editor/browser/config/configuration.ts @@ -90,6 +90,7 @@ export interface ISerializedFontInfo { readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; readonly typicalFullwidthCharacterWidth: number; + readonly canUseHalfwidthRightwardsArrow: boolean; readonly spaceWidth: number; readonly maxDigitWidth: number; } @@ -176,6 +177,7 @@ class CSSBasedConfiguration extends Disposable { isMonospace: readConfig.isMonospace, typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5), typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5), + canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow, spaceWidth: Math.max(readConfig.spaceWidth, 5), maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5), }, false); @@ -214,7 +216,9 @@ class CSSBasedConfiguration extends Disposable { const digit9 = this.createRequest('9', CharWidthRequestType.Regular, all, monospace); // monospace test: used for whitespace rendering - this.createRequest('→', CharWidthRequestType.Regular, all, monospace); + const rightwardsArrow = this.createRequest('→', CharWidthRequestType.Regular, all, monospace); + const halfwidthRightwardsArrow = this.createRequest('→', CharWidthRequestType.Regular, all, null); + this.createRequest('·', CharWidthRequestType.Regular, all, monospace); // monospace test: some characters @@ -256,6 +260,16 @@ class CSSBasedConfiguration extends Disposable { } } + let canUseHalfwidthRightwardsArrow = true; + if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) { + // using a halfwidth rightwards arrow would break monospace... + canUseHalfwidthRightwardsArrow = false; + } + if (halfwidthRightwardsArrow.width > rightwardsArrow.width) { + // using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow + canUseHalfwidthRightwardsArrow = false; + } + // let's trust the zoom level only 2s after it was changed. const canTrustBrowserZoomLevel = (browser.getTimeSinceLastZoomLevelChanged() > 2000); return new FontInfo({ @@ -268,6 +282,7 @@ class CSSBasedConfiguration extends Disposable { isMonospace: isMonospace, typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width, typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width, + canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow, spaceWidth: space.width, maxDigitWidth: maxDigitWidth }, canTrustBrowserZoomLevel); diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 05c024d8d8c..46a8671d6f4 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -5,6 +5,7 @@ 'use strict'; +import * as nls from 'vs/nls'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -16,7 +17,7 @@ import { registerEditorCommand, ICommandOptions, EditorCommand, Command } from ' import { IColumnSelectResult, ColumnSelection } from 'vs/editor/common/controller/cursorColumnSelection'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import H = editorCommon.Handler; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -26,8 +27,9 @@ import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { MenuId } from 'vs/platform/actions/common/actions'; -const CORE_WEIGHT = KeybindingsRegistry.WEIGHT.editorCore(); +const CORE_WEIGHT = KeybindingWeight.EditorCore; export abstract class CoreEditorCommand extends EditorCommand { public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { @@ -1620,7 +1622,7 @@ function findFocusedEditor(accessor: ServicesAccessor): ICodeEditor { } function registerCommand(command: Command) { - KeybindingsRegistry.registerCommandAndKeybindingRule(command.toCommandAndKeybindingRule(CORE_WEIGHT)); + command.register(); } /** @@ -1704,11 +1706,17 @@ registerCommand(new EditorOrNativeTextInputCommand({ editorHandler: CoreNavigationCommands.SelectAll, inputHandler: 'selectAll', id: 'editor.action.selectAll', - precondition: EditorContextKeys.focus, + precondition: EditorContextKeys.textInputFocus, kbOpts: { weight: CORE_WEIGHT, kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_A + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '1_basic', + title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), + order: 1 } })); @@ -1721,6 +1729,12 @@ registerCommand(new EditorOrNativeTextInputCommand({ weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_Z + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '1_do', + title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), + order: 1 } })); registerCommand(new EditorHandlerCommand('default:' + H.Undo, H.Undo)); @@ -1736,6 +1750,12 @@ registerCommand(new EditorOrNativeTextInputCommand({ primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z } + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '1_do', + title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), + order: 2 } })); registerCommand(new EditorHandlerCommand('default:' + H.Redo, H.Redo)); diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index fb902c2a458..d649911a352 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -8,7 +8,7 @@ import 'vs/css!./textAreaHandler'; import * as platform from 'vs/base/common/platform'; import * as browser from 'vs/base/browser/browser'; import * as strings from 'vs/base/common/strings'; -import { TextAreaInput, ITextAreaInputHost, IPasteData, ICompositionData } from 'vs/editor/browser/controller/textAreaInput'; +import { TextAreaInput, ITextAreaInputHost, IPasteData, ICompositionData, CopyOptions } from 'vs/editor/browser/controller/textAreaInput'; import { ISimpleModel, ITypeData, TextAreaState, PagedScreenReaderStrategy } from 'vs/editor/browser/controller/textAreaState'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -100,6 +100,7 @@ export class TextAreaHandler extends ViewPart { private _fontInfo: BareFontInfo; private _lineHeight: number; private _emptySelectionClipboard: boolean; + private _copyWithSyntaxHighlighting: boolean; /** * Defined only when the text area is visible (composition case). @@ -128,6 +129,7 @@ export class TextAreaHandler extends ViewPart { this._fontInfo = conf.fontInfo; this._lineHeight = conf.lineHeight; this._emptySelectionClipboard = conf.emptySelectionClipboard; + this._copyWithSyntaxHighlighting = conf.copyWithSyntaxHighlighting; this._visibleTextArea = null; this._selections = [new Selection(1, 1, 1, 1)]; @@ -191,6 +193,10 @@ export class TextAreaHandler extends ViewPart { }, getHTMLToCopy: (): string => { + if (!this._copyWithSyntaxHighlighting && !CopyOptions.forceCopyWithSyntaxHighlighting) { + return null; + } + return this._context.model.getHTMLToCopy(this._selections, this._emptySelectionClipboard); }, @@ -387,6 +393,9 @@ export class TextAreaHandler extends ViewPart { if (e.emptySelectionClipboard) { this._emptySelectionClipboard = conf.emptySelectionClipboard; } + if (e.copyWithSyntaxHighlighting) { + this._copyWithSyntaxHighlighting = conf.copyWithSyntaxHighlighting; + } return true; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 561d2a47ce6..53169924b40 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -267,7 +267,6 @@ export class TextAreaInput extends Disposable { } this._textAreaState = newState; - // console.log('==> DEDUCED INPUT: ' + JSON.stringify(typeInput)); if (this._nextCommand === ReadFromTextArea.Type) { if (typeInput.text !== '') { this._onType.fire(typeInput); diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index 09f54a8a013..fb3249c7903 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -121,7 +121,7 @@ export class TextAreaState { // See https://github.com/Microsoft/vscode/issues/42251 // where typing always happens at offset 0 in the textarea // when using a custom title area in OSX and moving the window - if (strings.endsWith(currentValue, previousValue)) { + if (!strings.startsWith(currentValue, previousValue) && strings.endsWith(currentValue, previousValue)) { // Looks like something was typed at offset 0 // ==> pretend we placed the cursor at offset 0 to begin with... previousSelectionStart = 0; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 559d6cdfc1e..d2b256b0ac9 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -396,6 +396,14 @@ export interface ICodeEditor extends editorCommon.IEditor { * @internal */ onDidType(listener: (text: string) => void): IDisposable; + /** + * An event emitted after composition has started. + */ + onCompositionStart(listener: () => void): IDisposable; + /** + * An event emitted after composition has ended. + */ + onCompositionEnd(listener: () => void): IDisposable; /** * An event emitted when editing failed because the editor is read-only. * @event @@ -578,9 +586,9 @@ export interface ICodeEditor extends editorCommon.IEditor { * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. * @param source The source of the call. * @param edits The edits to execute. - * @param endCursoState Cursor state after the edits were applied. + * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; /** * Execute multiple (concommitent) commands on the editor. @@ -853,4 +861,4 @@ export function getCodeEditor(thing: any): ICodeEditor { } return null; -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index b483b681901..48f8128addd 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -14,13 +14,11 @@ import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor'; */ export class PageCoordinates { _pageCoordinatesBrand: void; - public readonly x: number; - public readonly y: number; - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } + constructor( + public readonly x: number, + public readonly y: number + ) { } public toClientCoordinates(): ClientCoordinates { return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY); @@ -37,13 +35,10 @@ export class PageCoordinates { export class ClientCoordinates { _clientCoordinatesBrand: void; - public readonly clientX: number; - public readonly clientY: number; - - constructor(clientX: number, clientY: number) { - this.clientX = clientX; - this.clientY = clientY; - } + constructor( + public readonly clientX: number, + public readonly clientY: number + ) { } public toPageCoordinates(): PageCoordinates { return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY); @@ -56,17 +51,12 @@ export class ClientCoordinates { export class EditorPagePosition { _editorPagePositionBrand: void; - public readonly x: number; - public readonly y: number; - public readonly width: number; - public readonly height: number; - - constructor(x: number, y: number, width: number, height: number) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } + constructor( + public readonly x: number, + public readonly y: number, + public readonly width: number, + public readonly height: number + ) { } } export function createEditorPagePosition(editorViewDomNode: HTMLElement): EditorPagePosition { diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index c3de31ad7fb..fb90065a76e 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -5,21 +5,22 @@ 'use strict'; import { illegalArgument } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ServicesAccessor, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { KeybindingsRegistry, ICommandAndKeybindingRule, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Position } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { MenuId, MenuRegistry, IMenuItem } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel } from 'vs/editor/common/model'; +import { IPosition } from 'vs/base/browser/ui/contextview/contextview'; export type ServicesAccessor = ServicesAccessor; export type IEditorContributionCtor = IConstructorSignature1; @@ -28,53 +29,83 @@ export type IEditorContributionCtor = IConstructorSignature1 this.runCommand(accessor, args), - weight: weight, - when: kbWhen, - primary: kbOpts.primary, - secondary: kbOpts.secondary, - win: kbOpts.win, - linux: kbOpts.linux, - mac: kbOpts.mac, - description: this._description - }; + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: this.id, + handler: (accessor, args) => this.runCommand(accessor, args), + weight: this._kbOpts.weight, + when: kbWhen, + primary: this._kbOpts.primary, + secondary: this._kbOpts.secondary, + win: this._kbOpts.win, + linux: this._kbOpts.linux, + mac: this._kbOpts.mac, + description: this._description + }); + + } else { + + CommandsRegistry.registerCommand({ + id: this.id, + handler: (accessor, args) => this.runCommand(accessor, args), + description: this._description + }); + } } public abstract runCommand(accessor: ServicesAccessor, args: any): void | TPromise; @@ -143,8 +174,8 @@ export abstract class EditorCommand extends Command { //#region EditorAction export interface IEditorCommandMenuOptions { - group?: string; - order?: number; + group: string; + order: number; when?: ContextKeyExpr; } export interface IActionOptions extends ICommandOptions { @@ -165,20 +196,21 @@ export abstract class EditorAction extends EditorCommand { this.menuOpts = opts.menuOpts; } - public toMenuItem(): IMenuItem { - if (!this.menuOpts) { - return null; + public register(): void { + + if (this.menuOpts) { + MenuRegistry.appendMenuItem(MenuId.EditorContext, { + command: { + id: this.id, + title: this.label + }, + when: ContextKeyExpr.and(this.precondition, this.menuOpts.when), + group: this.menuOpts.group, + order: this.menuOpts.order + }); } - return { - command: { - id: this.id, - title: this.label - }, - when: ContextKeyExpr.and(this.precondition, this.menuOpts.when), - group: this.menuOpts.group, - order: this.menuOpts.order - }; + super.register(); } public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | TPromise { @@ -210,8 +242,14 @@ export function registerLanguageCommand(id: string, handler: (accessor: Services CommandsRegistry.registerCommand(id, (accessor, args) => handler(accessor, args || {})); } -export function registerDefaultLanguageCommand(id: string, handler: (model: ITextModel, position: Position, args: { [n: string]: any }) => any) { - registerLanguageCommand(id, function (accessor, args) { +interface IDefaultArgs { + resource: URI; + position: IPosition; + [name: string]: any; +} + +export function registerDefaultLanguageCommand(id: string, handler: (model: ITextModel, position: Position, args: IDefaultArgs) => any) { + registerLanguageCommand(id, function (accessor, args: IDefaultArgs) { const { resource, position } = args; if (!(resource instanceof URI)) { @@ -288,14 +326,7 @@ class EditorContributionRegistry { } public registerEditorAction(action: EditorAction) { - - let menuItem = action.toMenuItem(); - if (menuItem) { - MenuRegistry.appendMenuItem(MenuId.EditorContext, menuItem); - } - - KeybindingsRegistry.registerCommandAndKeybindingRule(action.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - + action.register(); this.editorActions.push(action); } @@ -308,7 +339,7 @@ class EditorContributionRegistry { } public registerEditorCommand(editorCommand: EditorCommand) { - KeybindingsRegistry.registerCommandAndKeybindingRule(editorCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + editorCommand.register(); this.editorCommands[editorCommand.id] = editorCommand; } diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 8bbde13a40f..c31b000de98 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -8,7 +8,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { WorkspaceEdit } from 'vs/editor/common/modes'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from '../editorBrowser'; -import { Selection } from 'vs/editor/common/core/selection'; import { IProgressRunner } from 'vs/platform/progress/common/progress'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); @@ -20,7 +19,6 @@ export interface IBulkEditOptions { } export interface IBulkEditResult { - selection: Selection; ariaSummary: string; } diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 9fad9a52fe4..7b006da43e7 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -5,7 +5,7 @@ 'use strict'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; import { IDecorationRenderOptions, IThemeDecorationRenderOptions, IContentDecorationRenderOptions, isThemeColor } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDecorationOverviewRulerOptions, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; @@ -212,7 +212,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { const _CSS_MAP: { [prop: string]: string; } = { color: 'color:{0} !important;', - opacity: 'opacity:{0};', + opacity: 'opacity:{0}; will-change: opacity;', // TODO@Ben: 'will-change: opacity' is a workaround for https://github.com/Microsoft/vscode/issues/52196 backgroundColor: 'background-color:{0};', outline: 'outline:{0};', diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 931531d2719..eefc44bb8e1 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; +import * as resources from 'vs/base/common/resources'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { normalize } from 'vs/base/common/paths'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -79,7 +79,7 @@ export class OpenerService implements IOpenerService { return TPromise.as(undefined); } else if (resource.scheme === Schemas.file) { - resource = resource.with({ path: normalize(resource.path) }); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) + resource = resources.normalizePath(resource); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) } promise = this._editorService.openCodeEditor({ resource, options: { selection, } }, this._editorService.getFocusedCodeEditor(), options && options.openToSide); } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 7509cb3f777..aef3582c374 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -22,6 +22,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { private _spaceWidth: number; private _renderResult: string[]; private _enabled: boolean; + private _activeIndentEnabled: boolean; constructor(context: ViewContext) { super(); @@ -30,6 +31,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { this._lineHeight = this._context.configuration.editor.lineHeight; this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth; this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides; + this._activeIndentEnabled = this._context.configuration.editor.viewInfo.highlightActiveIndentGuide; this._renderResult = null; this._context.addEventHandler(this); @@ -53,6 +55,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { } if (e.viewInfo) { this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides; + this._activeIndentEnabled = this._context.configuration.editor.viewInfo.highlightActiveIndentGuide; } return true; } @@ -114,7 +117,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { let activeIndentStartLineNumber = 0; let activeIndentEndLineNumber = 0; let activeIndentLevel = 0; - if (this._primaryLineNumber) { + if (this._activeIndentEnabled && this._primaryLineNumber) { const activeIndentInfo = this._context.model.getActiveIndentGuide(this._primaryLineNumber, visibleStartLineNumber, visibleEndLineNumber); activeIndentStartLineNumber = activeIndentInfo.startLineNumber; activeIndentEndLineNumber = activeIndentInfo.endLineNumber; diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 6e1338d6adb..01acc025bfe 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -74,6 +74,7 @@ export class ViewLineOptions { public readonly renderControlCharacters: boolean; public readonly spaceWidth: number; public readonly useMonospaceOptimizations: boolean; + public readonly canUseHalfwidthRightwardsArrow: boolean; public readonly lineHeight: number; public readonly stopRenderingLineAfter: number; public readonly fontLigatures: boolean; @@ -87,6 +88,7 @@ export class ViewLineOptions { config.editor.fontInfo.isMonospace && !config.editor.viewInfo.disableMonospaceOptimizations ); + this.canUseHalfwidthRightwardsArrow = config.editor.fontInfo.canUseHalfwidthRightwardsArrow; this.lineHeight = config.editor.lineHeight; this.stopRenderingLineAfter = config.editor.viewInfo.stopRenderingLineAfter; this.fontLigatures = config.editor.viewInfo.fontLigatures; @@ -99,6 +101,7 @@ export class ViewLineOptions { && this.renderControlCharacters === other.renderControlCharacters && this.spaceWidth === other.spaceWidth && this.useMonospaceOptimizations === other.useMonospaceOptimizations + && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow && this.lineHeight === other.lineHeight && this.stopRenderingLineAfter === other.stopRenderingLineAfter && this.fontLigatures === other.fontLigatures @@ -190,6 +193,7 @@ export class ViewLine implements IVisibleLine { let renderLineInput = new RenderLineInput( options.useMonospaceOptimizations, + options.canUseHalfwidthRightwardsArrow, lineData.content, lineData.continuesWithWrappedLine, lineData.isBasicASCII, diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e35b26ddd3a..410e034e8b4 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -46,12 +46,14 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { CoreEditorCommand } from 'vs/editor/browser/controller/coreCommands'; -import { editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoBorder, editorInfoForeground, editorHintForeground, editorHintBorder, editorUnnecessaryForeground } from 'vs/editor/common/view/editorColorRegistry'; +import { editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoBorder, editorInfoForeground, editorHintForeground, editorHintBorder, editorUnnecessaryCodeOpacity, editorUnnecessaryCodeBorder } from 'vs/editor/common/view/editorColorRegistry'; import { Color } from 'vs/base/common/color'; import { ClassName } from 'vs/editor/common/model/intervalTree'; let EDITOR_ID = 0; +const SHOW_UNUSED_ENABLED_CLASS = 'showUnused'; + export interface ICodeEditorWidgetOptions { /** * Is this a simple widget (not a real code editor) ? @@ -125,6 +127,12 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidType: Emitter = this._register(new Emitter()); public readonly onDidType = this._onDidType.event; + private readonly _onCompositionStart: Emitter = this._register(new Emitter()); + public readonly onCompositionStart = this._onCompositionStart.event; + + private readonly _onCompositionEnd: Emitter = this._register(new Emitter()); + public readonly onCompositionEnd = this._onCompositionEnd.event; + private readonly _onDidPaste: Emitter = this._register(new Emitter()); public readonly onDidPaste = this._onDidPaste.event; @@ -227,6 +235,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (e.layoutInfo) { this._onDidLayoutChange.fire(this._configuration.editor.layoutInfo); } + if (this._configuration.editor.showUnused) { + this.domElement.classList.add(SHOW_UNUSED_ENABLED_CLASS); + } else { + this.domElement.classList.remove(SHOW_UNUSED_ENABLED_CLASS); + } })); this._contextKeyService = this._register(contextKeyService.createScoped(this.domElement)); @@ -887,6 +900,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return; } + if (handlerId === editorCommon.Handler.CompositionStart) { + this._onCompositionStart.fire(); + } + if (handlerId === editorCommon.Handler.CompositionEnd) { + this._onCompositionEnd.fire(); + } + const action = this.getAction(handlerId); if (action) { TPromise.as(action.run()).then(null, onUnexpectedError); @@ -909,7 +929,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (command) { payload = payload || {}; payload.source = source; - TPromise.as(command.runEditorCommand(null, this, payload)).done(null, onUnexpectedError); + TPromise.as(command.runEditorCommand(null, this, payload)).then(null, onUnexpectedError); return true; } @@ -1349,22 +1369,22 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this.isSimpleWidget) { commandDelegate = { paste: (source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[]) => { - this.cursor.trigger(source, editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText }); + this.trigger(source, editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText }); }, type: (source: string, text: string) => { - this.cursor.trigger(source, editorCommon.Handler.Type, { text }); + this.trigger(source, editorCommon.Handler.Type, { text }); }, replacePreviousChar: (source: string, text: string, replaceCharCnt: number) => { - this.cursor.trigger(source, editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); + this.trigger(source, editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); }, compositionStart: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.CompositionStart, undefined); + this.trigger(source, editorCommon.Handler.CompositionStart, undefined); }, compositionEnd: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.CompositionEnd, undefined); + this.trigger(source, editorCommon.Handler.CompositionEnd, undefined); }, cut: (source: string) => { - this.cursor.trigger(source, editorCommon.Handler.Cut, undefined); + this.trigger(source, editorCommon.Handler.Cut, undefined); } }; } else { @@ -1543,6 +1563,8 @@ class EditorContextKeysManager extends Disposable { private _editorReadonly: IContextKey; private _hasMultipleSelections: IContextKey; private _hasNonEmptySelection: IContextKey; + private _canUndo: IContextKey; + private _canRedo: IContextKey; constructor( editor: CodeEditorWidget, @@ -1560,6 +1582,8 @@ class EditorContextKeysManager extends Disposable { this._editorReadonly = EditorContextKeys.readOnly.bindTo(contextKeyService); this._hasMultipleSelections = EditorContextKeys.hasMultipleSelections.bindTo(contextKeyService); this._hasNonEmptySelection = EditorContextKeys.hasNonEmptySelection.bindTo(contextKeyService); + this._canUndo = EditorContextKeys.canUndo.bindTo(contextKeyService); + this._canRedo = EditorContextKeys.canRedo.bindTo(contextKeyService); this._register(this._editor.onDidChangeConfiguration(() => this._updateFromConfig())); this._register(this._editor.onDidChangeCursorSelection(() => this._updateFromSelection())); @@ -1567,10 +1591,13 @@ class EditorContextKeysManager extends Disposable { this._register(this._editor.onDidBlurEditorWidget(() => this._updateFromFocus())); this._register(this._editor.onDidFocusEditorText(() => this._updateFromFocus())); this._register(this._editor.onDidBlurEditorText(() => this._updateFromFocus())); + this._register(this._editor.onDidChangeModel(() => this._updateFromModel())); + this._register(this._editor.onDidChangeConfiguration(() => this._updateFromModel())); this._updateFromConfig(); this._updateFromSelection(); this._updateFromFocus(); + this._updateFromModel(); } private _updateFromConfig(): void { @@ -1596,6 +1623,12 @@ class EditorContextKeysManager extends Disposable { this._editorTextFocus.set(this._editor.hasTextFocus() && !this._editor.isSimpleWidget); this._textInputFocus.set(this._editor.hasTextFocus()); } + + private _updateFromModel(): void { + const model = this._editor.getModel(); + this._canUndo.set(model && model.canUndo()); + this._canRedo.set(model && model.canRedo()); + } } export class EditorModeContext extends Disposable { @@ -1796,8 +1829,13 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .${ClassName.EditorHintDecoration} { background: url("data:image/svg+xml,${getDotDotDotSVGData(hintForeground)}") no-repeat bottom left; }`); } - const unnecessaryForeground = theme.getColor(editorUnnecessaryForeground); + const unnecessaryForeground = theme.getColor(editorUnnecessaryCodeOpacity); if (unnecessaryForeground) { - collector.addRule(`.monaco-editor .${ClassName.EditorUnnecessaryDecoration} { color: ${unnecessaryForeground}; }`); + collector.addRule(`.${SHOW_UNUSED_ENABLED_CLASS} .monaco-editor .${ClassName.EditorUnnecessaryInlineDecoration} { opacity: ${unnecessaryForeground.rgba.a}; will-change: opacity; }`); // TODO@Ben: 'will-change: opacity' is a workaround for https://github.com/Microsoft/vscode/issues/52196 + } + + const unnecessaryBorder = theme.getColor(editorUnnecessaryCodeBorder); + if (unnecessaryBorder) { + collector.addRule(`.${SHOW_UNUSED_ENABLED_CLASS} .monaco-editor .${ClassName.EditorUnnecessaryDecoration} { border-bottom: 2px dashed ${unnecessaryBorder}; }`); } }); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 9c3fba06d47..f2eebc3f3df 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -31,13 +31,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { Event, Emitter } from 'vs/base/common/event'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { registerThemingParticipant, IThemeService, ITheme, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; -import { scrollbarShadow, diffInserted, diffRemoved, defaultInsertColor, defaultRemoveColor, diffInsertedOutline, diffRemovedOutline } from 'vs/platform/theme/common/colorRegistry'; +import { scrollbarShadow, diffInserted, diffRemoved, defaultInsertColor, defaultRemoveColor, diffInsertedOutline, diffRemovedOutline, diffBorder } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IStringBuilder, createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { IModelDeltaDecoration, IModelDecorationsChangeAccessor, ITextModel } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -585,7 +585,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE // renderSideBySide if (renderSideBySideChanged) { if (this._renderSideBySide) { - this._setStrategy(new DiffEdtorWidgetSideBySide(this._createDataSource(), this._enableSplitViewResizing, )); + this._setStrategy(new DiffEdtorWidgetSideBySide(this._createDataSource(), this._enableSplitViewResizing)); } else { this._setStrategy(new DiffEdtorWidgetInline(this._createDataSource(), this._enableSplitViewResizing)); } @@ -944,7 +944,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE clonedOptions.folding = false; clonedOptions.codeLens = false; clonedOptions.fixedOverflowWidgets = true; - clonedOptions.lineDecorationsWidth = '2ch'; + // clonedOptions.lineDecorationsWidth = '2ch'; if (!clonedOptions.minimap) { clonedOptions.minimap = {}; } @@ -1177,7 +1177,7 @@ interface IDataSource { getModifiedEditor(): editorBrowser.ICodeEditor; } -abstract class DiffEditorWidgetStyle extends Disposable { +abstract class DiffEditorWidgetStyle extends Disposable implements IDiffEditorWidgetStyle { _dataSource: IDataSource; _insertColor: Color; @@ -1228,6 +1228,9 @@ abstract class DiffEditorWidgetStyle extends Disposable { protected abstract _getViewZones(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor, renderIndicators: boolean): IEditorsZones; protected abstract _getOriginalEditorDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorDiffDecorations; protected abstract _getModifiedEditorDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorDiffDecorations; + + public abstract setEnableSplitViewResizing(enableSplitViewResizing: boolean): void; + public abstract layout(): number; } interface IMyViewZone extends editorBrowser.IViewZone { @@ -1529,10 +1532,6 @@ class DiffEdtorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEd this._sash.onDidReset(() => this.onSashReset()); } - public dispose(): void { - super.dispose(); - } - public setEnableSplitViewResizing(enableSplitViewResizing: boolean): void { let newDisableSash = (enableSplitViewResizing === false); if (this._disableSash !== newDisableSash) { @@ -1778,10 +1777,6 @@ class DiffEdtorWidgetInline extends DiffEditorWidgetStyle implements IDiffEditor })); } - public dispose(): void { - super.dispose(); - } - public setEnableSplitViewResizing(enableSplitViewResizing: boolean): void { // Nothing to do.. } @@ -1987,6 +1982,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, originalModel.mightContainRTL()); const output = renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), + config.fontInfo.canUseHalfwidthRightwardsArrow, lineContent, false, isBasicASCII, @@ -2052,4 +2048,9 @@ registerThemingParticipant((theme, collector) => { if (shadow) { collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { box-shadow: -6px 0 5px -5px ${shadow}; }`); } + + let border = theme.getColor(diffBorder); + if (border) { + collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { border-left: 1px solid ${border}; }`); + } }); diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index c06799df4c5..58b825408e6 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -30,6 +30,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const DIFF_LINES_PADDING = 3; @@ -768,6 +769,7 @@ export class DiffReview extends Disposable { const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); const r = renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), + config.fontInfo.canUseHalfwidthRightwardsArrow, lineContent, false, isBasicASCII, @@ -810,7 +812,8 @@ class DiffReviewNext extends EditorAction { precondition: ContextKeyExpr.has('isInDiffEditor'), kbOpts: { kbExpr: null, - primary: KeyCode.F7 + primary: KeyCode.F7, + weight: KeybindingWeight.EditorContrib } }); } @@ -832,7 +835,8 @@ class DiffReviewPrev extends EditorAction { precondition: ContextKeyExpr.has('isInDiffEditor'), kbOpts: { kbExpr: null, - primary: KeyMod.Shift | KeyCode.F7 + primary: KeyMod.Shift | KeyCode.F7, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/browser/widget/media/diffEditor.css b/src/vs/editor/browser/widget/media/diffEditor.css index 08829aa00e7..5e71f38a9bd 100644 --- a/src/vs/editor/browser/widget/media/diffEditor.css +++ b/src/vs/editor/browser/widget/media/diffEditor.css @@ -41,6 +41,8 @@ opacity: 0.7; background-repeat: no-repeat; background-position: 50% 50%; + background-position: center; + background-size: 11px 11px; } .monaco-editor.hc-black .insert-sign, .monaco-diff-editor.hc-black .insert-sign, diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 05859b26f37..ef9f9fc914b 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -61,6 +61,8 @@ export interface IEnvConfiguration { accessibilitySupport: platform.AccessibilitySupport; } +const hasOwnProperty = Object.hasOwnProperty; + export abstract class CommonEditorConfiguration extends Disposable implements editorCommon.IConfiguration { protected _rawOptions: editorOptions.IEditorOptions; @@ -80,6 +82,8 @@ export abstract class CommonEditorConfiguration extends Disposable implements ed this._rawOptions.scrollbar = objects.mixin({}, this._rawOptions.scrollbar || {}); this._rawOptions.minimap = objects.mixin({}, this._rawOptions.minimap || {}); this._rawOptions.find = objects.mixin({}, this._rawOptions.find || {}); + this._rawOptions.hover = objects.mixin({}, this._rawOptions.hover || {}); + this._rawOptions.parameterHints = objects.mixin({}, this._rawOptions.parameterHints || {}); this._validatedOptions = editorOptions.EditorOptionsValidator.validate(this._rawOptions, EDITOR_DEFAULTS); this.editor = null; @@ -135,7 +139,53 @@ export abstract class CommonEditorConfiguration extends Disposable implements ed return editorOptions.InternalEditorOptionsFactory.createInternalEditorOptions(env, opts); } + private static _primitiveArrayEquals(a: any[], b: any[]): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; + } + + private static _subsetEquals(base: object, subset: object): boolean { + for (let key in subset) { + if (hasOwnProperty.call(subset, key)) { + const subsetValue = subset[key]; + const baseValue = base[key]; + + if (baseValue === subsetValue) { + continue; + } + if (Array.isArray(baseValue) && Array.isArray(subsetValue)) { + if (!this._primitiveArrayEquals(baseValue, subsetValue)) { + return false; + } + continue; + } + if (typeof baseValue === 'object' && typeof subsetValue === 'object') { + if (!this._subsetEquals(baseValue, subsetValue)) { + return false; + } + continue; + } + + return false; + } + } + return true; + } + public updateOptions(newOptions: editorOptions.IEditorOptions): void { + if (typeof newOptions === 'undefined') { + return; + } + if (CommonEditorConfiguration._subsetEquals(this._rawOptions, newOptions)) { + return; + } this._rawOptions = objects.mixin(this._rawOptions, newOptions || {}); this._validatedOptions = editorOptions.EditorOptionsValidator.validate(this._rawOptions, EDITOR_DEFAULTS); this._recomputeOptions(); @@ -197,7 +247,7 @@ const editorConfiguration: IConfigurationNode = { 'editor.lineHeight': { 'type': 'number', 'default': EDITOR_FONT_DEFAULTS.lineHeight, - 'description': nls.localize('lineHeight', "Controls the line height. Use 0 to compute the lineHeight from the fontSize.") + 'description': nls.localize('lineHeight', "Controls the line height. Use 0 to compute the line height from the font size.") }, 'editor.letterSpacing': { 'type': 'number', @@ -222,55 +272,53 @@ const editorConfiguration: IConfigurationNode = { 'type': 'number' }, 'default': EDITOR_DEFAULTS.viewInfo.rulers, - 'description': nls.localize('rulers', "Render vertical rulers after a certain number of monospace characters. Use multiple values for multiple rulers. No rulers are drawn if array is empty") + 'description': nls.localize('rulers', "Render vertical rulers after a certain number of monospace characters. Use multiple values for multiple rulers. No rulers are drawn if array is empty.") }, 'editor.wordSeparators': { 'type': 'string', 'default': EDITOR_DEFAULTS.wordSeparators, - 'description': nls.localize('wordSeparators', "Characters that will be used as word separators when doing word related navigations or operations") + 'description': nls.localize('wordSeparators', "Characters that will be used as word separators when doing word related navigations or operations.") }, 'editor.tabSize': { 'type': 'number', 'default': EDITOR_MODEL_DEFAULTS.tabSize, 'minimum': 1, - 'description': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), - 'errorMessage': nls.localize('tabSize.errorMessage', "Expected 'number'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") + 'markdownDescription': nls.localize('tabSize', "The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.") }, 'editor.insertSpaces': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.insertSpaces, - 'description': nls.localize('insertSpaces', "Insert spaces when pressing Tab. This setting is overridden based on the file contents when `editor.detectIndentation` is on."), - 'errorMessage': nls.localize('insertSpaces.errorMessage', "Expected 'boolean'. Note that the value \"auto\" has been replaced by the `editor.detectIndentation` setting.") + 'markdownDescription': nls.localize('insertSpaces', "Insert spaces when pressing `Tab`. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.") }, 'editor.detectIndentation': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.detectIndentation, - 'description': nls.localize('detectIndentation', "When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.") + 'markdownDescription': nls.localize('detectIndentation', "Controls whether `#editor.tabSize#` and `#editor.insertSpaces#` will be automatically detected when a file is opened based on the file contents.") }, 'editor.roundedSelection': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.roundedSelection, - 'description': nls.localize('roundedSelection', "Controls if selections have rounded corners") + 'description': nls.localize('roundedSelection', "Controls whether selections should have rounded corners.") }, 'editor.scrollBeyondLastLine': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.scrollBeyondLastLine, - 'description': nls.localize('scrollBeyondLastLine', "Controls if the editor will scroll beyond the last line") + 'description': nls.localize('scrollBeyondLastLine', "Controls whether the editor will scroll beyond the last line.") }, 'editor.scrollBeyondLastColumn': { 'type': 'number', 'default': EDITOR_DEFAULTS.viewInfo.scrollBeyondLastColumn, - 'description': nls.localize('scrollBeyondLastColumn', "Controls the number of extra characters beyond which the editor will scroll horizontally") + 'description': nls.localize('scrollBeyondLastColumn', "Controls the number of extra characters beyond which the editor will scroll horizontally.") }, 'editor.smoothScrolling': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.smoothScrolling, - 'description': nls.localize('smoothScrolling', "Controls if the editor will scroll using an animation") + 'description': nls.localize('smoothScrolling', "Controls whether the editor will scroll using an animation.") }, 'editor.minimap.enabled': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.minimap.enabled, - 'description': nls.localize('minimap.enabled', "Controls if the minimap is shown") + 'description': nls.localize('minimap.enabled', "Controls whether the minimap is shown.") }, 'editor.minimap.side': { 'type': 'string', @@ -287,33 +335,48 @@ const editorConfiguration: IConfigurationNode = { 'editor.minimap.renderCharacters': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.minimap.renderCharacters, - 'description': nls.localize('minimap.renderCharacters', "Render the actual characters on a line (as opposed to color blocks)") + 'description': nls.localize('minimap.renderCharacters', "Render the actual characters on a line as opposed to color blocks.") }, 'editor.minimap.maxColumn': { 'type': 'number', 'default': EDITOR_DEFAULTS.viewInfo.minimap.maxColumn, - 'description': nls.localize('minimap.maxColumn', "Limit the width of the minimap to render at most a certain number of columns") + 'description': nls.localize('minimap.maxColumn', "Limit the width of the minimap to render at most a certain number of columns.") + }, + 'editor.hover.enabled': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.contribInfo.hover.enabled, + 'description': nls.localize('hover.enabled', "Controls whether the hover is shown.") + }, + 'editor.hover.delay': { + 'type': 'number', + 'default': EDITOR_DEFAULTS.contribInfo.hover.delay, + 'description': nls.localize('hover.delay', "Time delay in milliseconds after which to the hover is shown.") + }, + 'editor.hover.sticky': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.contribInfo.hover.sticky, + 'description': nls.localize('hover.sticky', "Controls whether the hover should remain visible when mouse is moved over it.") }, 'editor.find.seedSearchStringFromSelection': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.seedSearchStringFromSelection, - 'description': nls.localize('find.seedSearchStringFromSelection', "Controls if we seed the search string in Find Widget from editor selection") + 'description': nls.localize('find.seedSearchStringFromSelection', "Controls whether the search string in the Find Widget is seeded from the editor selection.") }, 'editor.find.autoFindInSelection': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.autoFindInSelection, - 'description': nls.localize('find.autoFindInSelection', "Controls if Find in Selection flag is turned on when multiple characters or lines of text are selected in the editor") + 'description': nls.localize('find.autoFindInSelection', "Controls whether the find operation is carried on selected text or the entire file in the editor.") }, 'editor.find.globalFindClipboard': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.find.globalFindClipboard, - 'description': nls.localize('find.globalFindClipboard', "Controls if the Find Widget should read or modify the shared find clipboard on macOS"), + 'description': nls.localize('find.globalFindClipboard', "Controls whether the Find Widget should read or modify the shared find clipboard on macOS."), 'included': platform.isMacintosh }, 'editor.wordWrap': { 'type': 'string', 'enum': ['off', 'on', 'wordWrapColumn', 'bounded'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('wordWrap.off', "Lines will never wrap."), nls.localize('wordWrap.on', "Lines will wrap at the viewport width."), nls.localize({ @@ -321,14 +384,14 @@ const editorConfiguration: IConfigurationNode = { comment: [ '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Lines will wrap at `editor.wordWrapColumn`."), + }, "Lines will wrap at `#editor.wordWrapColumn#`."), nls.localize({ key: 'wordWrap.bounded', comment: [ '- viewport means the edge of the visible window size.', '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Lines will wrap at the minimum of viewport and `editor.wordWrapColumn`."), + }, "Lines will wrap at the minimum of viewport and `#editor.wordWrapColumn#`."), ], 'default': EDITOR_DEFAULTS.wordWrap, 'description': nls.localize({ @@ -337,46 +400,52 @@ const editorConfiguration: IConfigurationNode = { '- \'off\', \'on\', \'wordWrapColumn\' and \'bounded\' refer to values the setting can take and should not be localized.', '- `editor.wordWrapColumn` refers to a different setting and should not be localized.' ] - }, "Controls how lines should wrap. Can be:\n - 'off' (disable wrapping),\n - 'on' (viewport wrapping),\n - 'wordWrapColumn' (wrap at `editor.wordWrapColumn`) or\n - 'bounded' (wrap at minimum of viewport and `editor.wordWrapColumn`).") + }, "Controls how lines should wrap.") }, 'editor.wordWrapColumn': { 'type': 'integer', 'default': EDITOR_DEFAULTS.wordWrapColumn, 'minimum': 1, - 'description': nls.localize({ + 'markdownDescription': nls.localize({ key: 'wordWrapColumn', comment: [ '- `editor.wordWrap` refers to a different setting and should not be localized.', '- \'wordWrapColumn\' and \'bounded\' refer to values the different setting can take and should not be localized.' ] - }, "Controls the wrapping column of the editor when `editor.wordWrap` is 'wordWrapColumn' or 'bounded'.") + }, "Controls the wrapping column of the editor when `#editor.wordWrap#` is `wordWrapColumn` or `bounded`.") }, 'editor.wrappingIndent': { 'type': 'string', 'enum': ['none', 'same', 'indent', 'deepIndent'], + enumDescriptions: [ + nls.localize('wrappingIndent.none', "No indentation. Wrapped lines begin at column 1."), + nls.localize('wrappingIndent.same', "Wrapped lines get the same indentation as the parent."), + nls.localize('wrappingIndent.indent', "Wrapped lines get +1 indentation toward the parent."), + nls.localize('wrappingIndent.deepIndent', "Wrapped lines get +2 indentation toward the parent."), + ], 'default': 'same', - 'description': nls.localize('wrappingIndent', "Controls the indentation of wrapped lines. Can be one of 'none', 'same', 'indent' or 'deepIndent'.") + 'description': nls.localize('wrappingIndent', "Controls the indentation of wrapped lines."), }, 'editor.mouseWheelScrollSensitivity': { 'type': 'number', 'default': EDITOR_DEFAULTS.viewInfo.scrollbar.mouseWheelScrollSensitivity, - 'description': nls.localize('mouseWheelScrollSensitivity', "A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events") + 'markdownDescription': nls.localize('mouseWheelScrollSensitivity', "A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.") }, 'editor.multiCursorModifier': { 'type': 'string', 'enum': ['ctrlCmd', 'alt'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('multiCursorModifier.ctrlCmd', "Maps to `Control` on Windows and Linux and to `Command` on macOS."), nls.localize('multiCursorModifier.alt', "Maps to `Alt` on Windows and Linux and to `Option` on macOS.") ], 'default': 'alt', - 'description': nls.localize({ + 'markdownDescription': nls.localize({ key: 'multiCursorModifier', comment: [ '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add multiple cursors with the mouse. `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier.") + }, "The modifier to be used to add multiple cursors with the mouse. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).") }, 'editor.multiCursorMergeOverlapping': { 'type': 'boolean', @@ -410,54 +479,96 @@ const editorConfiguration: IConfigurationNode = { } ], 'default': EDITOR_DEFAULTS.contribInfo.quickSuggestions, - 'description': nls.localize('quickSuggestions', "Controls if suggestions should automatically show up while typing") + 'description': nls.localize('quickSuggestions', "Controls whether suggestions should automatically show up while typing.") }, 'editor.quickSuggestionsDelay': { 'type': 'integer', 'default': EDITOR_DEFAULTS.contribInfo.quickSuggestionsDelay, 'minimum': 0, - 'description': nls.localize('quickSuggestionsDelay', "Controls the delay in ms after which quick suggestions will show up") + 'description': nls.localize('quickSuggestionsDelay', "Controls the delay in milliseconds after which quick suggestions will show up.") }, - 'editor.parameterHints': { + 'editor.parameterHints.enabled': { 'type': 'boolean', - 'default': EDITOR_DEFAULTS.contribInfo.parameterHints, - 'description': nls.localize('parameterHints', "Enables pop-up that shows parameter documentation and type information as you type") + 'default': EDITOR_DEFAULTS.contribInfo.parameterHints.enabled, + 'description': nls.localize('parameterHints.enabled', "Enables a pop-up that shows parameter documentation and type information as you type.") + }, + 'editor.parameterHints.cycle': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.contribInfo.parameterHints.cycle, + 'description': nls.localize('parameterHints.cycle', "Controls whether the parameter hints menu cycles or closes when reaching the end of the list.") }, 'editor.autoClosingBrackets': { - 'type': 'boolean', + type: 'string', + enum: ['always', 'languageDefined', 'beforeWhitespace', 'never'], + enumDescriptions: [ + '', + nls.localize('editor.autoClosingBrackets.languageDefined', "Use language configurations to determine when to autoclose brackets."), + nls.localize('editor.autoClosingBrackets.beforeWhitespace', "Autoclose brackets only when the cursor is to the left of whitespace."), + '', + + ], 'default': EDITOR_DEFAULTS.autoClosingBrackets, - 'description': nls.localize('autoClosingBrackets', "Controls if the editor should automatically close brackets after opening them") + 'description': nls.localize('autoClosingBrackets', "Controls whether the editor should automatically close brackets after the user adds an opening bracket.") + }, + 'editor.autoClosingQuotes': { + type: 'string', + enum: ['always', 'languageDefined', 'beforeWhitespace', 'never'], + enumDescriptions: [ + '', + nls.localize('editor.autoClosingQuotes.languageDefined', "Use language configurations to determine when to autoclose quotes."), + nls.localize('editor.autoClosingQuotes.beforeWhitespace', "Autoclose quotes only when the cursor is to the left of whitespace."), + '', + ], + 'default': EDITOR_DEFAULTS.autoClosingQuotes, + 'description': nls.localize('autoClosingQuotes', "Controls whether the editor should automatically close quotes after the user adds an opening quote.") + }, + 'editor.autoSurround': { + type: 'string', + enum: ['languageDefined', 'brackets', 'quotes', 'never'], + enumDescriptions: [ + nls.localize('editor.autoSurround.languageDefined', "Use language configurations to determine when to automatically surround selections."), + nls.localize('editor.autoSurround.brackets', "Surround with brackets but not quotes."), + nls.localize('editor.autoSurround.quotes', "Surround with quotes but not brackets."), + '' + ], + 'default': EDITOR_DEFAULTS.autoSurround, + 'description': nls.localize('autoSurround', "Controls whether the editor should automatically surround selections.") }, 'editor.formatOnType': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.formatOnType, - 'description': nls.localize('formatOnType', "Controls if the editor should automatically format the line after typing") + 'description': nls.localize('formatOnType', "Controls whether the editor should automatically format the line after typing.") }, 'editor.formatOnPaste': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.formatOnPaste, - 'description': nls.localize('formatOnPaste', "Controls if the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document.") + 'description': nls.localize('formatOnPaste', "Controls whether the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document.") }, 'editor.autoIndent': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.autoIndent, - 'description': nls.localize('autoIndent', "Controls if the editor should automatically adjust the indentation when users type, paste or move lines. Indentation rules of the language must be available.") + 'description': nls.localize('autoIndent', "Controls whether the editor should automatically adjust the indentation when users type, paste or move lines. Extensions with indentation rules of the language must be available.") }, 'editor.suggestOnTriggerCharacters': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.suggestOnTriggerCharacters, - 'description': nls.localize('suggestOnTriggerCharacters', "Controls if suggestions should automatically show up when typing trigger characters") + 'description': nls.localize('suggestOnTriggerCharacters', "Controls whether suggestions should automatically show up when typing trigger characters.") }, 'editor.acceptSuggestionOnEnter': { 'type': 'string', 'enum': ['on', 'smart', 'off'], 'default': EDITOR_DEFAULTS.contribInfo.acceptSuggestionOnEnter, - 'description': nls.localize('acceptSuggestionOnEnter', "Controls if suggestions should be accepted on 'Enter' - in addition to 'Tab'. Helps to avoid ambiguity between inserting new lines or accepting suggestions. The value 'smart' means only accept a suggestion with Enter when it makes a textual change") + 'markdownEnumDescriptions': [ + '', + nls.localize('acceptSuggestionOnEnterSmart', "Only accept a suggestion with `Enter` when it makes a textual change."), + '' + ], + 'markdownDescription': nls.localize('acceptSuggestionOnEnter', "Controls whether suggestions should be accepted on `Enter`, in addition to `Tab`. Helps to avoid ambiguity between inserting new lines or accepting suggestions.") }, 'editor.acceptSuggestionOnCommitCharacter': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.acceptSuggestionOnCommitCharacter, - 'description': nls.localize('acceptSuggestionOnCommitCharacter', "Controls if suggestions should be accepted on commit characters. For instance in JavaScript the semi-colon (';') can be a commit character that accepts a suggestion and types that character.") + 'markdownDescription': nls.localize('acceptSuggestionOnCommitCharacter', "Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character.") }, 'editor.snippetSuggestions': { 'type': 'string', @@ -468,7 +579,7 @@ const editorConfiguration: IConfigurationNode = { nls.localize('snippetSuggestions.inline', "Show snippets suggestions with other suggestions."), nls.localize('snippetSuggestions.none', "Do not show snippet suggestions."), ], - 'default': EDITOR_DEFAULTS.contribInfo.snippetSuggestions, + 'default': EDITOR_DEFAULTS.contribInfo.suggest.snippets, 'description': nls.localize('snippetSuggestions', "Controls whether snippets are shown with other suggestions and how they are sorted.") }, 'editor.emptySelectionClipboard': { @@ -476,6 +587,11 @@ const editorConfiguration: IConfigurationNode = { 'default': EDITOR_DEFAULTS.emptySelectionClipboard, 'description': nls.localize('emptySelectionClipboard', "Controls whether copying without a selection copies the current line.") }, + 'editor.copyWithSyntaxHighlighting': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.copyWithSyntaxHighlighting, + 'description': nls.localize('copyWithSyntaxHighlighting', "Controls whether syntax highlighting should be copied into the clipboard.") + }, 'editor.wordBasedSuggestions': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.wordBasedSuggestions, @@ -484,7 +600,7 @@ const editorConfiguration: IConfigurationNode = { 'editor.suggestSelection': { 'type': 'string', 'enum': ['first', 'recentlyUsed', 'recentlyUsedByPrefix'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('suggestSelection.first', "Always select the first suggestion."), nls.localize('suggestSelection.recentlyUsed', "Select recent suggestions unless further typing selects one, e.g. `console.| -> console.log` because `log` has been completed recently."), nls.localize('suggestSelection.recentlyUsedByPrefix', "Select suggestions based on previous prefixes that have completed those suggestions, e.g. `co -> console` and `con -> const`."), @@ -496,33 +612,43 @@ const editorConfiguration: IConfigurationNode = { 'type': 'integer', 'default': 0, 'minimum': 0, - 'description': nls.localize('suggestFontSize', "Font size for the suggest widget") + 'markdownDescription': nls.localize('suggestFontSize', "Font size for the suggest widget. When set to `0`, the value of `#editor.fontSize#` is used.") }, 'editor.suggestLineHeight': { 'type': 'integer', 'default': 0, 'minimum': 0, - 'description': nls.localize('suggestLineHeight', "Line height for the suggest widget") + 'markdownDescription': nls.localize('suggestLineHeight', "Line height for the suggest widget. When set to `0`, the value of `#editor.lineHeight#` is used.") + }, + 'editor.suggest.filterGraceful': { + type: 'boolean', + default: true, + description: nls.localize('suggest.filterGraceful', "Controls whether filtering and sorting suggestions accounts for small typos.") + }, + 'editor.suggest.snippetsPreventQuickSuggestions': { + type: 'boolean', + default: true, + description: nls.localize('suggest.snippetsPreventQuickSuggestions', "Control whether an active snippet prevents quick suggestions.") }, 'editor.selectionHighlight': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.selectionHighlight, - 'description': nls.localize('selectionHighlight', "Controls whether the editor should highlight similar matches to the selection") + 'description': nls.localize('selectionHighlight', "Controls whether the editor should highlight matches similar to the selection") }, 'editor.occurrencesHighlight': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.occurrencesHighlight, - 'description': nls.localize('occurrencesHighlight', "Controls whether the editor should highlight semantic symbol occurrences") + 'description': nls.localize('occurrencesHighlight', "Controls whether the editor should highlight semantic symbol occurrences.") }, 'editor.overviewRulerLanes': { 'type': 'integer', 'default': 3, - 'description': nls.localize('overviewRulerLanes', "Controls the number of decorations that can show up at the same position in the overview ruler") + 'description': nls.localize('overviewRulerLanes', "Controls the number of decorations that can show up at the same position in the overview ruler.") }, 'editor.overviewRulerBorder': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.overviewRulerBorder, - 'description': nls.localize('overviewRulerBorder', "Controls if a border should be drawn around the overview ruler.") + 'description': nls.localize('overviewRulerBorder', "Controls whether a border should be drawn around the overview ruler.") }, 'editor.cursorBlinking': { 'type': 'string', @@ -533,55 +659,71 @@ const editorConfiguration: IConfigurationNode = { 'editor.mouseWheelZoom': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.mouseWheelZoom, - 'description': nls.localize('mouseWheelZoom', "Zoom the font of the editor when using mouse wheel and holding Ctrl") + 'markdownDescription': nls.localize('mouseWheelZoom', "Zoom the font of the editor when using mouse wheel and holding `Ctrl`.") }, 'editor.cursorStyle': { 'type': 'string', 'enum': ['block', 'block-outline', 'line', 'line-thin', 'underline', 'underline-thin'], 'default': editorOptions.cursorStyleToString(EDITOR_DEFAULTS.viewInfo.cursorStyle), - 'description': nls.localize('cursorStyle', "Controls the cursor style, accepted values are 'block', 'block-outline', 'line', 'line-thin', 'underline' and 'underline-thin'") + 'description': nls.localize('cursorStyle', "Controls the cursor style.") }, 'editor.cursorWidth': { 'type': 'integer', 'default': EDITOR_DEFAULTS.viewInfo.cursorWidth, - 'description': nls.localize('cursorWidth', "Controls the width of the cursor when editor.cursorStyle is set to 'line'") + 'markdownDescription': nls.localize('cursorWidth', "Controls the width of the cursor when `#editor.cursorStyle#` is set to `line`.") }, 'editor.fontLigatures': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.fontLigatures, - 'description': nls.localize('fontLigatures', "Enables font ligatures") + 'description': nls.localize('fontLigatures', "Enables/Disables font ligatures.") }, 'editor.hideCursorInOverviewRuler': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.viewInfo.hideCursorInOverviewRuler, - 'description': nls.localize('hideCursorInOverviewRuler', "Controls if the cursor should be hidden in the overview ruler.") + 'description': nls.localize('hideCursorInOverviewRuler', "Controls whether the cursor should be hidden in the overview ruler.") }, 'editor.renderWhitespace': { 'type': 'string', 'enum': ['none', 'boundary', 'all'], + 'enumDescriptions': [ + '', + nls.localize('renderWhiteSpace.boundary', "Render whitespace characters except for single spaces between words."), + '' + ], default: EDITOR_DEFAULTS.viewInfo.renderWhitespace, - description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters, possibilities are 'none', 'boundary', and 'all'. The 'boundary' option does not render single spaces between words.") + description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters.") }, 'editor.renderControlCharacters': { 'type': 'boolean', default: EDITOR_DEFAULTS.viewInfo.renderControlCharacters, - description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters") + description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters.") }, 'editor.renderIndentGuides': { 'type': 'boolean', default: EDITOR_DEFAULTS.viewInfo.renderIndentGuides, - description: nls.localize('renderIndentGuides', "Controls whether the editor should render indent guides") + description: nls.localize('renderIndentGuides', "Controls whether the editor should render indent guides.") + }, + 'editor.highlightActiveIndentGuide': { + 'type': 'boolean', + default: EDITOR_DEFAULTS.viewInfo.highlightActiveIndentGuide, + description: nls.localize('highlightActiveIndentGuide', "Controls whether the editor should highlight the active indent guide.") }, 'editor.renderLineHighlight': { 'type': 'string', 'enum': ['none', 'gutter', 'line', 'all'], + 'enumDescriptions': [ + '', + '', + '', + nls.localize('renderLineHighlight.all', "Highlights both the gutter and the current line."), + ], default: EDITOR_DEFAULTS.viewInfo.renderLineHighlight, - description: nls.localize('renderLineHighlight', "Controls how the editor should render the current line highlight, possibilities are 'none', 'gutter', 'line', and 'all'.") + description: nls.localize('renderLineHighlight', "Controls how the editor should render the current line highlight.") }, 'editor.codeLens': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.codeLens, - 'description': nls.localize('codeLens', "Controls if the editor shows CodeLens") + 'description': nls.localize('codeLens', "Controls whether the editor shows CodeLens") }, 'editor.folding': { 'type': 'boolean', @@ -591,12 +733,8 @@ const editorConfiguration: IConfigurationNode = { 'editor.foldingStrategy': { 'type': 'string', 'enum': ['auto', 'indentation'], - 'enumDescriptions': [ - nls.localize('foldingStrategyAuto', 'If available, use a language specific folding strategy, otherwise falls back to the indentation based strategy.'), - nls.localize('foldingStrategyIndentation', 'Always use the indentation based folding strategy') - ], 'default': EDITOR_DEFAULTS.contribInfo.foldingStrategy, - 'description': nls.localize('foldingStrategy', "Controls the way folding ranges are computed. 'auto' picks uses a language specific folding strategy, if available. 'indentation' forces that the indentation based folding strategy is used.") + 'markdownDescription': nls.localize('foldingStrategy', "Controls the strategy for computing folding ranges. `auto` uses a language specific folding strategy, if available. `indentation` uses the indentation based folding strategy.") }, 'editor.showFoldingControls': { 'type': 'string', @@ -617,22 +755,22 @@ const editorConfiguration: IConfigurationNode = { 'editor.useTabStops': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.useTabStops, - 'description': nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops") + 'description': nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops.") }, 'editor.trimAutoWhitespace': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.trimAutoWhitespace, - 'description': nls.localize('trimAutoWhitespace', "Remove trailing auto inserted whitespace") + 'description': nls.localize('trimAutoWhitespace', "Remove trailing auto inserted whitespace.") }, 'editor.stablePeek': { 'type': 'boolean', 'default': false, - 'description': nls.localize('stablePeek', "Keep peek editors open even when double clicking their content or when hitting Escape.") + 'markdownDescription': nls.localize('stablePeek', "Keep peek editors open even when double clicking their content or when hitting `Escape`.") }, 'editor.dragAndDrop': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.dragAndDrop, - 'description': nls.localize('dragAndDrop', "Controls if the editor should allow to move selections via drag and drop.") + 'description': nls.localize('dragAndDrop', "Controls whether the editor should allow moving selections via drag and drop.") }, 'editor.accessibilitySupport': { 'type': 'string', @@ -645,10 +783,15 @@ const editorConfiguration: IConfigurationNode = { 'default': EDITOR_DEFAULTS.accessibilitySupport, 'description': nls.localize('accessibilitySupport', "Controls whether the editor should run in a mode where it is optimized for screen readers.") }, + 'editor.showUnused': { + 'type': 'boolean', + 'default': EDITOR_DEFAULTS.showUnused, + 'description': nls.localize('showUnused', "Controls fading out of unused code.") + }, 'editor.links': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.links, - 'description': nls.localize('links', "Controls whether the editor should detect links and make them clickable") + 'description': nls.localize('links', "Controls whether the editor should detect links and make them clickable.") }, 'editor.colorDecorators': { 'type': 'boolean', @@ -658,14 +801,14 @@ const editorConfiguration: IConfigurationNode = { 'editor.lightbulb.enabled': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.lightbulbEnabled, - 'description': nls.localize('codeActions', "Enables the code action lightbulb") + 'description': nls.localize('codeActions', "Enables the code action lightbulb in the editor.") }, 'editor.codeActionsOnSave': { 'type': 'object', 'properties': { 'source.organizeImports': { 'type': 'boolean', - 'description': nls.localize('codeActionsOnSave.organizeImports', "Run organize imports on save?") + 'description': nls.localize('codeActionsOnSave.organizeImports', "Controls whether organize imports action should be run on file save.") } }, 'additionalProperties': { @@ -677,23 +820,23 @@ const editorConfiguration: IConfigurationNode = { 'editor.codeActionsOnSaveTimeout': { 'type': 'number', 'default': EDITOR_DEFAULTS.contribInfo.codeActionsOnSaveTimeout, - 'description': nls.localize('codeActionsOnSaveTimeout', "Timeout for code actions run on save.") + 'description': nls.localize('codeActionsOnSaveTimeout', "Timeout in milliseconds after which the code actions that are run on save are cancelled.") }, 'editor.selectionClipboard': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.selectionClipboard, - 'description': nls.localize('selectionClipboard', "Controls if the Linux primary clipboard should be supported."), + 'description': nls.localize('selectionClipboard', "Controls whether the Linux primary clipboard should be supported."), 'included': platform.isLinux }, 'diffEditor.renderSideBySide': { 'type': 'boolean', 'default': true, - 'description': nls.localize('sideBySide', "Controls if the diff editor shows the diff side by side or inline") + 'description': nls.localize('sideBySide', "Controls whether the diff editor shows the diff side by side or inline.") }, 'diffEditor.ignoreTrimWhitespace': { 'type': 'boolean', 'default': true, - 'description': nls.localize('ignoreTrimWhitespace', "Controls if the diff editor shows changes in leading or trailing whitespace as diffs") + 'description': nls.localize('ignoreTrimWhitespace', "Controls whether the diff editor shows changes in leading or trailing whitespace as diffs.") }, 'editor.largeFileOptimizations': { 'type': 'boolean', @@ -703,7 +846,7 @@ const editorConfiguration: IConfigurationNode = { 'diffEditor.renderIndicators': { 'type': 'boolean', 'default': true, - 'description': nls.localize('renderIndicators', "Controls if the diff editor shows +/- indicators for added/removed changes") + 'description': nls.localize('renderIndicators', "Controls whether the diff editor shows +/- indicators for added/removed changes.") } } }; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2e0412c8732..602afeebbc4 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -95,13 +95,23 @@ export interface IEditorFindOptions { globalFindClipboard: boolean; } +/** + * Configuration options for auto closing quotes and brackets + */ +export type EditorAutoClosingStrategy = 'always' | 'languageDefined' | 'beforeWhitespace' | 'never'; + +/** + * Configuration options for auto wrapping quotes and brackets + */ +export type EditorAutoSurroundStrategy = 'languageDefined' | 'quotes' | 'brackets' | 'never'; + /** * Configuration options for editor minimap */ export interface IEditorMinimapOptions { /** * Enable the rendering of the minimap. - * Defaults to false. + * Defaults to true. */ enabled?: boolean; /** @@ -137,6 +147,54 @@ export interface IEditorLightbulbOptions { enabled?: boolean; } +/** + * Configuration options for editor hover + */ +export interface IEditorHoverOptions { + /** + * Enable the hover. + * Defaults to true. + */ + enabled?: boolean; + /** + * Delay for showing the hover. + * Defaults to 300. + */ + delay?: number; + /** + * Is the hover sticky such that it can be clicked and its contents selected? + * Defaults to true. + */ + sticky?: boolean; +} + +/** + * Configuration options for parameter hints + */ +export interface IEditorParameterHintOptions { + /** + * Enable parameter hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Enable cycling of parameter hints. + * Defaults to false. + */ + cycle?: boolean; +} + +export interface ISuggestOptions { + /** + * Enable graceful matching. Defaults to true. + */ + filterGraceful?: boolean; + /** + * Prevent quick suggestions when a snippet is active. Defaults to true. + */ + snippetsPreventQuickSuggestions?: boolean; +} + /** * Configuration map for codeActionsOnSave */ @@ -367,10 +425,9 @@ export interface IEditorOptions { */ stopRenderingLineAfter?: number; /** - * Enable hover. - * Defaults to true. + * Configure the editor's hover. */ - hover?: boolean; + hover?: IEditorHoverOptions; /** * Enable detecting links and making them clickable. * Defaults to true. @@ -405,6 +462,10 @@ export interface IEditorOptions { * Defaults to 'auto'. It is best to leave this to 'auto'. */ accessibilitySupport?: 'auto' | 'off' | 'on'; + /** + * Suggest options. + */ + suggest?: ISuggestOptions; /** * Enable quick suggestions (shadow suggestions) * Defaults to true. @@ -416,19 +477,29 @@ export interface IEditorOptions { */ quickSuggestionsDelay?: number; /** - * Enables parameter hints + * Parameter hint options. */ - parameterHints?: boolean; + parameterHints?: IEditorParameterHintOptions; /** * Render icons in suggestions box. * Defaults to true. */ iconsInSuggestions?: boolean; /** - * Enable auto closing brackets. - * Defaults to true. + * Options for auto closing brackets. + * Defaults to language defined behavior. */ - autoClosingBrackets?: boolean; + autoClosingBrackets?: EditorAutoClosingStrategy; + /** + * Options for auto closing quotes. + * Defaults to language defined behavior. + */ + autoClosingQuotes?: EditorAutoClosingStrategy; + /** + * Options for auto surrounding. + * Defaults to always allowing auto surrounding. + */ + autoSurround?: EditorAutoSurroundStrategy; /** * Enable auto indentation adjustment. * Defaults to false. @@ -472,6 +543,10 @@ export interface IEditorOptions { * Copying without a selection copies the current line. */ emptySelectionClipboard?: boolean; + /** + * Syntax highlighting is copied. + */ + copyWithSyntaxHighlighting?: boolean; /** * Enable word based suggestions. Defaults to 'true' */ @@ -549,9 +624,14 @@ export interface IEditorOptions { renderControlCharacters?: boolean; /** * Enable rendering of indent guides. - * Defaults to false. + * Defaults to true. */ renderIndentGuides?: boolean; + /** + * Enable highlighting of the active indent guide. + * Defaults to true. + */ + highlightActiveIndentGuide?: boolean; /** * Enable rendering of current line highlight. * Defaults to all. @@ -581,6 +661,10 @@ export interface IEditorOptions { * The letter spacing */ letterSpacing?: number; + /** + * Controls fading out of unused variables. + */ + showUnused?: boolean; } /** @@ -795,6 +879,23 @@ export interface InternalEditorFindOptions { readonly globalFindClipboard: boolean; } +export interface InternalEditorHoverOptions { + readonly enabled: boolean; + readonly delay: number; + readonly sticky: boolean; +} + +export interface InternalSuggestOptions { + readonly filterGraceful: boolean; + readonly snippets: 'top' | 'bottom' | 'inline' | 'none'; + readonly snippetsPreventQuickSuggestions: boolean; +} + +export interface InternalParameterHintOptions { + readonly enabled: boolean; + readonly cycle: boolean; +} + export interface EditorWrappingInfo { readonly inDiffEditor: boolean; readonly isDominatedByLongLines: boolean; @@ -841,6 +942,7 @@ export interface InternalEditorViewOptions { readonly renderControlCharacters: boolean; readonly fontLigatures: boolean; readonly renderIndentGuides: boolean; + readonly highlightActiveIndentGuide: boolean; readonly renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; readonly scrollbar: InternalEditorScrollbarOptions; readonly minimap: InternalEditorMinimapOptions; @@ -849,23 +951,24 @@ export interface InternalEditorViewOptions { export interface EditorContribOptions { readonly selectionClipboard: boolean; - readonly hover: boolean; + readonly hover: InternalEditorHoverOptions; readonly links: boolean; readonly contextmenu: boolean; readonly quickSuggestions: boolean | { other: boolean, comments: boolean, strings: boolean }; readonly quickSuggestionsDelay: number; - readonly parameterHints: boolean; + readonly parameterHints: InternalParameterHintOptions; readonly iconsInSuggestions: boolean; readonly formatOnType: boolean; readonly formatOnPaste: boolean; readonly suggestOnTriggerCharacters: boolean; readonly acceptSuggestionOnEnter: 'on' | 'smart' | 'off'; readonly acceptSuggestionOnCommitCharacter: boolean; - readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; + // readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; readonly wordBasedSuggestions: boolean; readonly suggestSelection: 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'; readonly suggestFontSize: number; readonly suggestLineHeight: number; + readonly suggest: InternalSuggestOptions; readonly selectionHighlight: boolean; readonly occurrencesHighlight: boolean; readonly codeLens: boolean; @@ -901,14 +1004,18 @@ export interface IValidatedEditorOptions { readonly wordWrapBreakBeforeCharacters: string; readonly wordWrapBreakAfterCharacters: string; readonly wordWrapBreakObtrusiveCharacters: string; - readonly autoClosingBrackets: boolean; + readonly autoClosingBrackets: EditorAutoClosingStrategy; + readonly autoClosingQuotes: EditorAutoClosingStrategy; + readonly autoSurround: EditorAutoSurroundStrategy; readonly autoIndent: boolean; readonly dragAndDrop: boolean; readonly emptySelectionClipboard: boolean; + readonly copyWithSyntaxHighlighting: boolean; readonly useTabStops: boolean; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; readonly multiCursorMergeOverlapping: boolean; readonly accessibilitySupport: 'auto' | 'off' | 'on'; + readonly showUnused: boolean; readonly viewInfo: InternalEditorViewOptions; readonly contribInfo: EditorContribOptions; @@ -931,15 +1038,19 @@ export class InternalEditorOptions { readonly accessibilitySupport: platform.AccessibilitySupport; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; readonly multiCursorMergeOverlapping: boolean; + readonly showUnused: boolean; // ---- cursor options readonly wordSeparators: string; - readonly autoClosingBrackets: boolean; + readonly autoClosingBrackets: EditorAutoClosingStrategy; + readonly autoClosingQuotes: EditorAutoClosingStrategy; + readonly autoSurround: EditorAutoSurroundStrategy; readonly autoIndent: boolean; readonly useTabStops: boolean; readonly tabFocusMode: boolean; readonly dragAndDrop: boolean; readonly emptySelectionClipboard: boolean; + readonly copyWithSyntaxHighlighting: boolean; // ---- grouped options readonly layoutInfo: EditorLayoutInfo; @@ -961,17 +1072,21 @@ export class InternalEditorOptions { multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; multiCursorMergeOverlapping: boolean; wordSeparators: string; - autoClosingBrackets: boolean; + autoClosingBrackets: EditorAutoClosingStrategy; + autoClosingQuotes: EditorAutoClosingStrategy; + autoSurround: EditorAutoSurroundStrategy; autoIndent: boolean; useTabStops: boolean; tabFocusMode: boolean; dragAndDrop: boolean; emptySelectionClipboard: boolean; + copyWithSyntaxHighlighting: boolean; layoutInfo: EditorLayoutInfo; fontInfo: FontInfo; viewInfo: InternalEditorViewOptions; wrappingInfo: EditorWrappingInfo; contribInfo: EditorContribOptions; + showUnused: boolean; }) { this.canUseLayerHinting = source.canUseLayerHinting; this.pixelRatio = source.pixelRatio; @@ -983,16 +1098,20 @@ export class InternalEditorOptions { this.multiCursorMergeOverlapping = source.multiCursorMergeOverlapping; this.wordSeparators = source.wordSeparators; this.autoClosingBrackets = source.autoClosingBrackets; + this.autoClosingQuotes = source.autoClosingQuotes; + this.autoSurround = source.autoSurround; this.autoIndent = source.autoIndent; this.useTabStops = source.useTabStops; this.tabFocusMode = source.tabFocusMode; this.dragAndDrop = source.dragAndDrop; this.emptySelectionClipboard = source.emptySelectionClipboard; + this.copyWithSyntaxHighlighting = source.copyWithSyntaxHighlighting; this.layoutInfo = source.layoutInfo; this.fontInfo = source.fontInfo; this.viewInfo = source.viewInfo; this.wrappingInfo = source.wrappingInfo; this.contribInfo = source.contribInfo; + this.showUnused = source.showUnused; } /** @@ -1010,11 +1129,15 @@ export class InternalEditorOptions { && this.multiCursorMergeOverlapping === other.multiCursorMergeOverlapping && this.wordSeparators === other.wordSeparators && this.autoClosingBrackets === other.autoClosingBrackets + && this.autoClosingQuotes === other.autoClosingQuotes + && this.autoSurround === other.autoSurround && this.autoIndent === other.autoIndent && this.useTabStops === other.useTabStops && this.tabFocusMode === other.tabFocusMode && this.dragAndDrop === other.dragAndDrop + && this.showUnused === other.showUnused && this.emptySelectionClipboard === other.emptySelectionClipboard + && this.copyWithSyntaxHighlighting === other.copyWithSyntaxHighlighting && InternalEditorOptions._equalsLayoutInfo(this.layoutInfo, other.layoutInfo) && this.fontInfo.equals(other.fontInfo) && InternalEditorOptions._equalsViewOptions(this.viewInfo, other.viewInfo) @@ -1038,11 +1161,14 @@ export class InternalEditorOptions { multiCursorMergeOverlapping: (this.multiCursorMergeOverlapping !== newOpts.multiCursorMergeOverlapping), wordSeparators: (this.wordSeparators !== newOpts.wordSeparators), autoClosingBrackets: (this.autoClosingBrackets !== newOpts.autoClosingBrackets), + autoClosingQuotes: (this.autoClosingQuotes !== newOpts.autoClosingQuotes), + autoSurround: (this.autoSurround !== newOpts.autoSurround), autoIndent: (this.autoIndent !== newOpts.autoIndent), useTabStops: (this.useTabStops !== newOpts.useTabStops), tabFocusMode: (this.tabFocusMode !== newOpts.tabFocusMode), dragAndDrop: (this.dragAndDrop !== newOpts.dragAndDrop), emptySelectionClipboard: (this.emptySelectionClipboard !== newOpts.emptySelectionClipboard), + copyWithSyntaxHighlighting: (this.copyWithSyntaxHighlighting !== newOpts.copyWithSyntaxHighlighting), layoutInfo: (!InternalEditorOptions._equalsLayoutInfo(this.layoutInfo, newOpts.layoutInfo)), fontInfo: (!this.fontInfo.equals(newOpts.fontInfo)), viewInfo: (!InternalEditorOptions._equalsViewOptions(this.viewInfo, newOpts.viewInfo)), @@ -1122,6 +1248,7 @@ export class InternalEditorOptions { && a.renderControlCharacters === b.renderControlCharacters && a.fontLigatures === b.fontLigatures && a.renderIndentGuides === b.renderIndentGuides + && a.highlightActiveIndentGuide === b.highlightActiveIndentGuide && a.renderLineHighlight === b.renderLineHighlight && this._equalsScrollbarOptions(a.scrollbar, b.scrollbar) && this._equalsMinimapOptions(a.minimap, b.minimap) @@ -1165,7 +1292,6 @@ export class InternalEditorOptions { /** * @internal */ - private static _equalFindOptions(a: InternalEditorFindOptions, b: InternalEditorFindOptions): boolean { return ( a.seedSearchStringFromSelection === b.seedSearchStringFromSelection @@ -1174,6 +1300,42 @@ export class InternalEditorOptions { ); } + /** + * @internal + */ + private static _equalsParameterHintOptions(a: InternalParameterHintOptions, b: InternalParameterHintOptions): boolean { + return ( + a.enabled === b.enabled + && a.cycle === b.cycle + ); + } + + /** + * @internal + */ + private static _equalsHoverOptions(a: InternalEditorHoverOptions, b: InternalEditorHoverOptions): boolean { + return ( + a.enabled === b.enabled + && a.delay === b.delay + && a.sticky === b.sticky + ); + } + + /** + * @internal + */ + private static _equalsSuggestOptions(a: InternalSuggestOptions, b: InternalSuggestOptions): any { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.filterGraceful === b.filterGraceful + && a.snippets === b.snippets + && a.snippetsPreventQuickSuggestions === b.snippetsPreventQuickSuggestions; + } + } + /** * @internal */ @@ -1197,23 +1359,23 @@ export class InternalEditorOptions { private static _equalsContribOptions(a: EditorContribOptions, b: EditorContribOptions): boolean { return ( a.selectionClipboard === b.selectionClipboard - && a.hover === b.hover + && this._equalsHoverOptions(a.hover, b.hover) && a.links === b.links && a.contextmenu === b.contextmenu && InternalEditorOptions._equalsQuickSuggestions(a.quickSuggestions, b.quickSuggestions) && a.quickSuggestionsDelay === b.quickSuggestionsDelay - && a.parameterHints === b.parameterHints + && this._equalsParameterHintOptions(a.parameterHints, b.parameterHints) && a.iconsInSuggestions === b.iconsInSuggestions && a.formatOnType === b.formatOnType && a.formatOnPaste === b.formatOnPaste && a.suggestOnTriggerCharacters === b.suggestOnTriggerCharacters && a.acceptSuggestionOnEnter === b.acceptSuggestionOnEnter && a.acceptSuggestionOnCommitCharacter === b.acceptSuggestionOnCommitCharacter - && a.snippetSuggestions === b.snippetSuggestions && a.wordBasedSuggestions === b.wordBasedSuggestions && a.suggestSelection === b.suggestSelection && a.suggestFontSize === b.suggestFontSize && a.suggestLineHeight === b.suggestLineHeight + && this._equalsSuggestOptions(a.suggest, b.suggest) && a.selectionHighlight === b.selectionHighlight && a.occurrencesHighlight === b.occurrencesHighlight && a.codeLens === b.codeLens @@ -1383,11 +1545,14 @@ export interface IConfigurationChangedEvent { readonly multiCursorMergeOverlapping: boolean; readonly wordSeparators: boolean; readonly autoClosingBrackets: boolean; + readonly autoClosingQuotes: boolean; + readonly autoSurround: boolean; readonly autoIndent: boolean; readonly useTabStops: boolean; readonly tabFocusMode: boolean; readonly dragAndDrop: boolean; readonly emptySelectionClipboard: boolean; + readonly copyWithSyntaxHighlighting: boolean; readonly layoutInfo: boolean; readonly fontInfo: boolean; readonly viewInfo: boolean; @@ -1561,6 +1726,20 @@ export class EditorOptionsValidator { } const multiCursorModifier = _stringSet<'altKey' | 'metaKey' | 'ctrlKey'>(configuredMulticursorModifier, defaults.multiCursorModifier, ['altKey', 'metaKey', 'ctrlKey']); + let autoClosingBrackets: EditorAutoClosingStrategy; + let autoClosingQuotes: EditorAutoClosingStrategy; + let autoSurround: EditorAutoSurroundStrategy; + if (typeof opts.autoClosingBrackets === 'boolean' && opts.autoClosingBrackets === false) { + // backwards compatibility: disable all on boolean false + autoClosingBrackets = 'never'; + autoClosingQuotes = 'never'; + autoSurround = 'never'; + } else { + autoClosingBrackets = _stringSet(opts.autoClosingBrackets, defaults.autoClosingBrackets, ['always', 'languageDefined', 'beforeWhitespace', 'never']); + autoClosingQuotes = _stringSet(opts.autoClosingQuotes, defaults.autoClosingQuotes, ['always', 'languageDefined', 'beforeWhitespace', 'never']); + autoSurround = _stringSet(opts.autoSurround, defaults.autoSurround, ['languageDefined', 'brackets', 'quotes', 'never']); + } + return { inDiffEditor: _boolean(opts.inDiffEditor, defaults.inDiffEditor), wordSeparators: _string(opts.wordSeparators, defaults.wordSeparators), @@ -1577,14 +1756,18 @@ export class EditorOptionsValidator { wordWrapBreakBeforeCharacters: _string(opts.wordWrapBreakBeforeCharacters, defaults.wordWrapBreakBeforeCharacters), wordWrapBreakAfterCharacters: _string(opts.wordWrapBreakAfterCharacters, defaults.wordWrapBreakAfterCharacters), wordWrapBreakObtrusiveCharacters: _string(opts.wordWrapBreakObtrusiveCharacters, defaults.wordWrapBreakObtrusiveCharacters), - autoClosingBrackets: _boolean(opts.autoClosingBrackets, defaults.autoClosingBrackets), + autoClosingBrackets, + autoClosingQuotes, + autoSurround, autoIndent: _boolean(opts.autoIndent, defaults.autoIndent), dragAndDrop: _boolean(opts.dragAndDrop, defaults.dragAndDrop), emptySelectionClipboard: _boolean(opts.emptySelectionClipboard, defaults.emptySelectionClipboard), + copyWithSyntaxHighlighting: _boolean(opts.copyWithSyntaxHighlighting, defaults.copyWithSyntaxHighlighting), useTabStops: _boolean(opts.useTabStops, defaults.useTabStops), multiCursorModifier: multiCursorModifier, multiCursorMergeOverlapping: _boolean(opts.multiCursorMergeOverlapping, defaults.multiCursorMergeOverlapping), accessibilitySupport: _stringSet<'auto' | 'on' | 'off'>(opts.accessibilitySupport, defaults.accessibilitySupport, ['auto', 'on', 'off']), + showUnused: _boolean(opts.showUnused, defaults.showUnused), viewInfo: viewInfo, contribInfo: contribInfo, }; @@ -1642,6 +1825,46 @@ export class EditorOptionsValidator { }; } + private static _sanitizeParameterHintOpts(opts: IEditorParameterHintOptions, defaults: InternalParameterHintOptions): InternalParameterHintOptions { + if (typeof opts !== 'object') { + return defaults; + } + + return { + enabled: _boolean(opts.enabled, defaults.enabled), + cycle: _boolean(opts.cycle, defaults.cycle) + }; + } + + private static _santizeHoverOpts(_opts: boolean | IEditorHoverOptions, defaults: InternalEditorHoverOptions): InternalEditorHoverOptions { + let opts: IEditorHoverOptions; + if (typeof _opts === 'boolean') { + opts = { + enabled: _opts + }; + } else if (typeof _opts === 'object') { + opts = _opts; + } else { + return defaults; + } + + return { + enabled: _boolean(opts.enabled, defaults.enabled), + delay: _clampedInt(opts.delay, defaults.delay, 0, 10000), + sticky: _boolean(opts.sticky, defaults.sticky) + }; + } + + private static _sanitizeSuggestOpts(opts: IEditorOptions, defaults: InternalSuggestOptions): InternalSuggestOptions { + const suggestOpts = opts.suggest || {}; + return { + filterGraceful: _boolean(suggestOpts.filterGraceful, defaults.filterGraceful), + snippets: _stringSet<'top' | 'bottom' | 'inline' | 'none'>(opts.snippetSuggestions, defaults.snippets, ['top', 'bottom', 'inline', 'none']), + snippetsPreventQuickSuggestions: _boolean(suggestOpts.snippetsPreventQuickSuggestions, defaults.filterGraceful), + }; + } + + private static _sanitizeViewInfo(opts: IEditorOptions, defaults: InternalEditorViewOptions): InternalEditorViewOptions { let rulers: number[] = []; @@ -1738,6 +1961,7 @@ export class EditorOptionsValidator { renderControlCharacters: _boolean(opts.renderControlCharacters, defaults.renderControlCharacters), fontLigatures: fontLigatures, renderIndentGuides: _boolean(opts.renderIndentGuides, defaults.renderIndentGuides), + highlightActiveIndentGuide: _boolean(opts.highlightActiveIndentGuide, defaults.highlightActiveIndentGuide), renderLineHighlight: renderLineHighlight, scrollbar: scrollbar, minimap: minimap, @@ -1759,23 +1983,23 @@ export class EditorOptionsValidator { const find = this._santizeFindOpts(opts.find, defaults.find); return { selectionClipboard: _boolean(opts.selectionClipboard, defaults.selectionClipboard), - hover: _boolean(opts.hover, defaults.hover), + hover: this._santizeHoverOpts(opts.hover, defaults.hover), links: _boolean(opts.links, defaults.links), contextmenu: _boolean(opts.contextmenu, defaults.contextmenu), quickSuggestions: quickSuggestions, quickSuggestionsDelay: _clampedInt(opts.quickSuggestionsDelay, defaults.quickSuggestionsDelay, Constants.MIN_SAFE_SMALL_INTEGER, Constants.MAX_SAFE_SMALL_INTEGER), - parameterHints: _boolean(opts.parameterHints, defaults.parameterHints), + parameterHints: this._sanitizeParameterHintOpts(opts.parameterHints, defaults.parameterHints), iconsInSuggestions: _boolean(opts.iconsInSuggestions, defaults.iconsInSuggestions), formatOnType: _boolean(opts.formatOnType, defaults.formatOnType), formatOnPaste: _boolean(opts.formatOnPaste, defaults.formatOnPaste), suggestOnTriggerCharacters: _boolean(opts.suggestOnTriggerCharacters, defaults.suggestOnTriggerCharacters), acceptSuggestionOnEnter: _stringSet<'on' | 'smart' | 'off'>(opts.acceptSuggestionOnEnter, defaults.acceptSuggestionOnEnter, ['on', 'smart', 'off']), acceptSuggestionOnCommitCharacter: _boolean(opts.acceptSuggestionOnCommitCharacter, defaults.acceptSuggestionOnCommitCharacter), - snippetSuggestions: _stringSet<'top' | 'bottom' | 'inline' | 'none'>(opts.snippetSuggestions, defaults.snippetSuggestions, ['top', 'bottom', 'inline', 'none']), wordBasedSuggestions: _boolean(opts.wordBasedSuggestions, defaults.wordBasedSuggestions), suggestSelection: _stringSet<'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'>(opts.suggestSelection, defaults.suggestSelection, ['first', 'recentlyUsed', 'recentlyUsedByPrefix']), suggestFontSize: _clampedInt(opts.suggestFontSize, defaults.suggestFontSize, 0, 1000), suggestLineHeight: _clampedInt(opts.suggestLineHeight, defaults.suggestLineHeight, 0, 1000), + suggest: this._sanitizeSuggestOpts(opts, defaults.suggest), selectionHighlight: _boolean(opts.selectionHighlight, defaults.selectionHighlight), occurrencesHighlight: _boolean(opts.occurrencesHighlight, defaults.occurrencesHighlight), codeLens: _boolean(opts.codeLens, defaults.codeLens), @@ -1817,13 +2041,17 @@ export class InternalEditorOptionsFactory { wordWrapBreakAfterCharacters: opts.wordWrapBreakAfterCharacters, wordWrapBreakObtrusiveCharacters: opts.wordWrapBreakObtrusiveCharacters, autoClosingBrackets: opts.autoClosingBrackets, + autoClosingQuotes: opts.autoClosingQuotes, + autoSurround: opts.autoSurround, autoIndent: opts.autoIndent, dragAndDrop: opts.dragAndDrop, emptySelectionClipboard: opts.emptySelectionClipboard, + copyWithSyntaxHighlighting: opts.copyWithSyntaxHighlighting, useTabStops: opts.useTabStops, multiCursorModifier: opts.multiCursorModifier, multiCursorMergeOverlapping: opts.multiCursorMergeOverlapping, accessibilitySupport: opts.accessibilitySupport, + showUnused: opts.showUnused, viewInfo: { extraEditorClassName: opts.viewInfo.extraEditorClassName, @@ -1851,6 +2079,7 @@ export class InternalEditorOptionsFactory { renderControlCharacters: (accessibilityIsOn ? false : opts.viewInfo.renderControlCharacters), // DISABLED WHEN SCREEN READER IS ATTACHED fontLigatures: (accessibilityIsOn ? false : opts.viewInfo.fontLigatures), // DISABLED WHEN SCREEN READER IS ATTACHED renderIndentGuides: (accessibilityIsOn ? false : opts.viewInfo.renderIndentGuides), // DISABLED WHEN SCREEN READER IS ATTACHED + highlightActiveIndentGuide: opts.viewInfo.highlightActiveIndentGuide, renderLineHighlight: opts.viewInfo.renderLineHighlight, scrollbar: opts.viewInfo.scrollbar, minimap: { @@ -1877,11 +2106,11 @@ export class InternalEditorOptionsFactory { suggestOnTriggerCharacters: opts.contribInfo.suggestOnTriggerCharacters, acceptSuggestionOnEnter: opts.contribInfo.acceptSuggestionOnEnter, acceptSuggestionOnCommitCharacter: opts.contribInfo.acceptSuggestionOnCommitCharacter, - snippetSuggestions: opts.contribInfo.snippetSuggestions, wordBasedSuggestions: opts.contribInfo.wordBasedSuggestions, suggestSelection: opts.contribInfo.suggestSelection, suggestFontSize: opts.contribInfo.suggestFontSize, suggestLineHeight: opts.contribInfo.suggestLineHeight, + suggest: opts.contribInfo.suggest, selectionHighlight: (accessibilityIsOn ? false : opts.contribInfo.selectionHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED occurrencesHighlight: (accessibilityIsOn ? false : opts.contribInfo.occurrencesHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED codeLens: (accessibilityIsOn ? false : opts.contribInfo.codeLens), // DISABLED WHEN SCREEN READER IS ATTACHED @@ -2036,16 +2265,20 @@ export class InternalEditorOptionsFactory { multiCursorMergeOverlapping: opts.multiCursorMergeOverlapping, wordSeparators: opts.wordSeparators, autoClosingBrackets: opts.autoClosingBrackets, + autoClosingQuotes: opts.autoClosingQuotes, + autoSurround: opts.autoSurround, autoIndent: opts.autoIndent, useTabStops: opts.useTabStops, tabFocusMode: opts.readOnly ? true : env.tabFocusMode, dragAndDrop: opts.dragAndDrop, emptySelectionClipboard: opts.emptySelectionClipboard && env.emptySelectionClipboard, + copyWithSyntaxHighlighting: opts.copyWithSyntaxHighlighting, layoutInfo: layoutInfo, fontInfo: env.fontInfo, viewInfo: opts.viewInfo, wrappingInfo: wrappingInfo, - contribInfo: opts.contribInfo + contribInfo: opts.contribInfo, + showUnused: opts.showUnused, }); } } @@ -2218,9 +2451,9 @@ export class EditorLayoutProvider { } } -const DEFAULT_WINDOWS_FONT_FAMILY = 'Consolas, \'Courier New\', monospace, \'Segoe UI Emoji\''; -const DEFAULT_MAC_FONT_FAMILY = 'Menlo, Monaco, \'Courier New\', monospace, \'Apple Color Emoji\''; -const DEFAULT_LINUX_FONT_FAMILY = '\'Droid Sans Mono\', \'monospace\', monospace, \'Droid Sans Fallback\', \'Noto Color Emoji\''; +const DEFAULT_WINDOWS_FONT_FAMILY = 'Consolas, \'Courier New\', monospace'; +const DEFAULT_MAC_FONT_FAMILY = 'Menlo, Monaco, \'Courier New\', monospace'; +const DEFAULT_LINUX_FONT_FAMILY = '\'Droid Sans Mono\', \'monospace\', monospace, \'Droid Sans Fallback\''; /** * @internal @@ -2267,14 +2500,18 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { wordWrapBreakBeforeCharacters: '([{‘“〈《「『【〔([{「£¥$£¥++', wordWrapBreakAfterCharacters: ' \t})]?|&,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」', wordWrapBreakObtrusiveCharacters: '.', - autoClosingBrackets: true, + autoClosingBrackets: 'languageDefined', + autoClosingQuotes: 'languageDefined', + autoSurround: 'languageDefined', autoIndent: true, dragAndDrop: true, emptySelectionClipboard: true, + copyWithSyntaxHighlighting: true, useTabStops: true, multiCursorModifier: 'altKey', multiCursorMergeOverlapping: true, accessibilitySupport: 'auto', + showUnused: true, viewInfo: { extraEditorClassName: '', @@ -2302,6 +2539,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { renderControlCharacters: false, fontLigatures: false, renderIndentGuides: true, + highlightActiveIndentGuide: true, renderLineHighlight: 'line', scrollbar: { vertical: ScrollbarVisibility.Auto, @@ -2329,23 +2567,34 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { contribInfo: { selectionClipboard: true, - hover: true, + hover: { + enabled: true, + delay: 300, + sticky: true + }, links: true, contextmenu: true, quickSuggestions: { other: true, comments: false, strings: false }, quickSuggestionsDelay: 10, - parameterHints: true, + parameterHints: { + enabled: true, + cycle: false + }, iconsInSuggestions: true, formatOnType: false, formatOnPaste: false, suggestOnTriggerCharacters: true, acceptSuggestionOnEnter: 'on', acceptSuggestionOnCommitCharacter: true, - snippetSuggestions: 'inline', wordBasedSuggestions: true, suggestSelection: 'recentlyUsed', suggestFontSize: 0, suggestLineHeight: 0, + suggest: { + filterGraceful: true, + snippets: 'inline', + snippetsPreventQuickSuggestions: true + }, selectionHighlight: true, occurrencesHighlight: true, codeLens: true, diff --git a/src/vs/editor/common/config/fontInfo.ts b/src/vs/editor/common/config/fontInfo.ts index 24b105bbd4e..1c5b063979b 100644 --- a/src/vs/editor/common/config/fontInfo.ts +++ b/src/vs/editor/common/config/fontInfo.ts @@ -155,6 +155,7 @@ export class FontInfo extends BareFontInfo { readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; readonly typicalFullwidthCharacterWidth: number; + readonly canUseHalfwidthRightwardsArrow: boolean; readonly spaceWidth: number; readonly maxDigitWidth: number; @@ -171,6 +172,7 @@ export class FontInfo extends BareFontInfo { isMonospace: boolean; typicalHalfwidthCharacterWidth: number; typicalFullwidthCharacterWidth: number; + canUseHalfwidthRightwardsArrow: boolean; spaceWidth: number; maxDigitWidth: number; }, isTrusted: boolean) { @@ -179,6 +181,7 @@ export class FontInfo extends BareFontInfo { this.isMonospace = opts.isMonospace; this.typicalHalfwidthCharacterWidth = opts.typicalHalfwidthCharacterWidth; this.typicalFullwidthCharacterWidth = opts.typicalFullwidthCharacterWidth; + this.canUseHalfwidthRightwardsArrow = opts.canUseHalfwidthRightwardsArrow; this.spaceWidth = opts.spaceWidth; this.maxDigitWidth = opts.maxDigitWidth; } @@ -195,6 +198,7 @@ export class FontInfo extends BareFontInfo { && this.letterSpacing === other.letterSpacing && this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth && this.typicalFullwidthCharacterWidth === other.typicalFullwidthCharacterWidth + && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow && this.spaceWidth === other.spaceWidth && this.maxDigitWidth === other.maxDigitWidth ); diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 6e30401ddf9..caafa214f69 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -551,7 +551,6 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { private _type(source: string, text: string): void { if (!this._isDoingComposition && source === 'keyboard') { - console.log('keyboard', source, text); // If this event is coming straight from the keyboard, look for electric characters and enter for (let i = 0, len = text.length; i < len; i++) { diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index 0563b6a0771..ebf8dc4a689 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -15,7 +15,7 @@ import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageCo import { onUnexpectedError } from 'vs/base/common/errors'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration'; -import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; +import { IConfigurationChangedEvent, EditorAutoClosingStrategy, EditorAutoSurroundStrategy } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; @@ -66,6 +66,10 @@ export interface CharacterMap { [char: string]: string; } +const autoCloseAlways = _ => true; +const autoCloseNever = _ => false; +const autoCloseBeforeWhitespace = (chr: string) => (chr === ' ' || chr === '\t'); + export class CursorConfiguration { _cursorMoveConfigurationBrand: void; @@ -78,12 +82,16 @@ export class CursorConfiguration { public readonly useTabStops: boolean; public readonly wordSeparators: string; public readonly emptySelectionClipboard: boolean; + public readonly copyWithSyntaxHighlighting: boolean; public readonly multiCursorMergeOverlapping: boolean; - public readonly autoClosingBrackets: boolean; + public readonly autoClosingBrackets: EditorAutoClosingStrategy; + public readonly autoClosingQuotes: EditorAutoClosingStrategy; + public readonly autoSurround: EditorAutoSurroundStrategy; public readonly autoIndent: boolean; public readonly autoClosingPairsOpen: CharacterMap; public readonly autoClosingPairsClose: CharacterMap; public readonly surroundingPairs: CharacterMap; + public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean }; private readonly _languageIdentifier: LanguageIdentifier; private _electricChars: { [key: string]: boolean; }; @@ -95,6 +103,8 @@ export class CursorConfiguration { || e.emptySelectionClipboard || e.multiCursorMergeOverlapping || e.autoClosingBrackets + || e.autoClosingQuotes + || e.autoSurround || e.useTabStops || e.lineHeight || e.readOnly @@ -120,8 +130,11 @@ export class CursorConfiguration { this.useTabStops = c.useTabStops; this.wordSeparators = c.wordSeparators; this.emptySelectionClipboard = c.emptySelectionClipboard; + this.copyWithSyntaxHighlighting = c.copyWithSyntaxHighlighting; this.multiCursorMergeOverlapping = c.multiCursorMergeOverlapping; this.autoClosingBrackets = c.autoClosingBrackets; + this.autoClosingQuotes = c.autoClosingQuotes; + this.autoSurround = c.autoSurround; this.autoIndent = c.autoIndent; this.autoClosingPairsOpen = {}; @@ -129,6 +142,11 @@ export class CursorConfiguration { this.surroundingPairs = {}; this._electricChars = null; + this.shouldAutoCloseBefore = { + quote: CursorConfiguration._getShouldAutoClose(languageIdentifier, this.autoClosingQuotes), + bracket: CursorConfiguration._getShouldAutoClose(languageIdentifier, this.autoClosingBrackets) + }; + let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier); if (autoClosingPairs) { for (let i = 0; i < autoClosingPairs.length; i++) { @@ -180,6 +198,29 @@ export class CursorConfiguration { } } + private static _getShouldAutoClose(languageIdentifier: LanguageIdentifier, autoCloseConfig: EditorAutoClosingStrategy): (ch: string) => boolean { + switch (autoCloseConfig) { + case 'beforeWhitespace': + return autoCloseBeforeWhitespace; + case 'languageDefined': + return CursorConfiguration._getLanguageDefinedShouldAutoClose(languageIdentifier); + case 'always': + return autoCloseAlways; + case 'never': + return autoCloseNever; + } + } + + private static _getLanguageDefinedShouldAutoClose(languageIdentifier: LanguageIdentifier): (ch: string) => boolean { + try { + const autoCloseBeforeSet = LanguageConfigurationRegistry.getAutoCloseBeforeSet(languageIdentifier.id); + return c => autoCloseBeforeSet.indexOf(c) !== -1; + } catch (e) { + onUnexpectedError(e); + return autoCloseNever; + } + } + private static _getSurroundingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] { try { return LanguageConfigurationRegistry.getSurroundingPairs(languageIdentifier.id); @@ -537,3 +578,7 @@ export class CursorColumns { return column - 1 - (column - 1) % tabSize; } } + +export function isQuote(ch: string): boolean { + return (ch === '\'' || ch === '"' || ch === '`'); +} diff --git a/src/vs/editor/common/controller/cursorDeleteOperations.ts b/src/vs/editor/common/controller/cursorDeleteOperations.ts index e5b498b40e9..e16c0944cb9 100644 --- a/src/vs/editor/common/controller/cursorDeleteOperations.ts +++ b/src/vs/editor/common/controller/cursorDeleteOperations.ts @@ -5,7 +5,7 @@ 'use strict'; import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; -import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult, EditOperationType } from 'vs/editor/common/controller/cursorCommon'; +import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult, EditOperationType, isQuote } from 'vs/editor/common/controller/cursorCommon'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations'; @@ -49,7 +49,7 @@ export class DeleteOperations { } private static _isAutoClosingPairDelete(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): boolean { - if (!config.autoClosingBrackets) { + if (config.autoClosingBrackets === 'never' && config.autoClosingQuotes === 'never') { return false; } @@ -68,6 +68,16 @@ export class DeleteOperations { return false; } + if (isQuote(character)) { + if (config.autoClosingQuotes === 'never') { + return false; + } + } else { + if (config.autoClosingBrackets === 'never') { + return false; + } + } + const afterCharacter = lineText[position.column - 1]; const closeCharacter = config.autoClosingPairsOpen[character]; diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index bb9c031c095..039f6706bd2 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -607,7 +607,7 @@ export namespace CursorMove { \`\`\` 'left', 'right', 'up', 'down' 'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter' - 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter', + 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter' 'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside' \`\`\` * 'by': Unit to move. Default is computed based on 'to' value. diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 7085aa16c93..f18e3e9d8fc 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -6,7 +6,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { ReplaceCommand, ReplaceCommandWithoutChangingPosition, ReplaceCommandWithOffsetCursorState } from 'vs/editor/common/commands/replaceCommand'; -import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult, EditOperationType } from 'vs/editor/common/controller/cursorCommon'; +import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult, EditOperationType, isQuote } from 'vs/editor/common/controller/cursorCommon'; import { Range } from 'vs/editor/common/core/range'; import { ICommand } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; @@ -437,7 +437,9 @@ export class TypeOperations { } private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!config.autoClosingBrackets || !config.autoClosingPairsClose.hasOwnProperty(ch)) { + const autoCloseConfig = isQuote(ch) ? config.autoClosingQuotes : config.autoClosingBrackets; + + if (autoCloseConfig === 'never' || !config.autoClosingPairsClose.hasOwnProperty(ch)) { return false; } @@ -511,10 +513,15 @@ export class TypeOperations { } private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!config.autoClosingBrackets || !config.autoClosingPairsOpen.hasOwnProperty(ch)) { + const chIsQuote = isQuote(ch); + const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets; + + if (autoCloseConfig === 'never' || !config.autoClosingPairsOpen.hasOwnProperty(ch)) { return false; } + let shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket; + for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; if (!selection.isEmpty()) { @@ -525,7 +532,7 @@ export class TypeOperations { const lineText = model.getLineContent(position.lineNumber); // Do not auto-close ' or " after a word character - if ((ch === '\'' || ch === '"') && position.column > 1) { + if (chIsQuote && position.column > 1) { const wordSeparators = getMapForWordSeparators(config.wordSeparators); const characterBeforeCode = lineText.charCodeAt(position.column - 2); const characterBeforeType = wordSeparators.get(characterBeforeCode); @@ -538,7 +545,8 @@ export class TypeOperations { const characterAfter = lineText.charAt(position.column - 1); if (characterAfter) { let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter); - if (!isBeforeCloseBrace && !/\s/.test(characterAfter)) { + + if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { return false; } } @@ -579,12 +587,21 @@ export class TypeOperations { }); } + private static _shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { + if (isQuote(ch)) { + return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); + } else { + // Character is a bracket + return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); + } + } + private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!config.autoClosingBrackets || !config.surroundingPairs.hasOwnProperty(ch)) { + if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { return false; } - const isTypingAQuoteCharacter = (ch === '\'' || ch === '"'); + const isTypingAQuoteCharacter = isQuote(ch); for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; @@ -613,7 +630,7 @@ export class TypeOperations { if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { const selectionText = model.getValueInRange(selection); - if ((selectionText === '\'' || selectionText === '"')) { + if (isQuote(selectionText)) { // Typing a quote character on top of another quote character // => disable surround selection type return false; @@ -708,7 +725,7 @@ export class TypeOperations { } public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[]): EditOperationResult { - if (!config.autoClosingBrackets) { + if (config.autoClosingQuotes === 'never') { return null; } @@ -736,7 +753,7 @@ export class TypeOperations { // As we are not typing in a new character, so we don't need to run `_runAutoClosingCloseCharType` // Next step, let's try to check if it's an open char. if (config.autoClosingPairsOpen.hasOwnProperty(ch)) { - if ((ch === '\'' || ch === '"') && position.column > 2) { + if (isQuote(ch) && position.column > 2) { const wordSeparators = getMapForWordSeparators(config.wordSeparators); const characterBeforeCode = lineText.charCodeAt(position.column - 3); const characterBeforeType = wordSeparators.get(characterBeforeCode); @@ -749,7 +766,14 @@ export class TypeOperations { if (characterAfter) { let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter); - if (!isBeforeCloseBrace && !/\s/.test(characterAfter)) { + let shouldAutoCloseBefore = isQuote(ch) ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket; + if (isBeforeCloseBrace) { + // In normal auto closing logic, we will auto close if the cursor is even before a closing brace intentionally. + // However for composition mode, we do nothing here as users might clear all the characters for composition and we don't want to do a unnecessary auto close. + // Related: microsoft/vscode#57250. + continue; + } + if (!shouldAutoCloseBefore(characterAfter)) { continue; } } diff --git a/src/vs/editor/common/controller/cursorWordOperations.ts b/src/vs/editor/common/controller/cursorWordOperations.ts index 8bd73b791f0..1fc71edf103 100644 --- a/src/vs/editor/common/controller/cursorWordOperations.ts +++ b/src/vs/editor/common/controller/cursorWordOperations.ts @@ -10,6 +10,7 @@ import { WordCharacterClassifier, WordCharacterClass, getMapForWordSeparators } import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { CharCode } from 'vs/base/common/charCode'; interface IFindWordResult { /** @@ -38,7 +39,8 @@ const enum WordType { export const enum WordNavigationType { WordStart = 0, - WordEnd = 1 + WordStartFast = 1, + WordEnd = 2 } export class WordOperations { @@ -160,9 +162,11 @@ export class WordOperations { public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { let lineNumber = position.lineNumber; let column = position.column; + let movedToPreviousLine = false; if (column === 1) { if (lineNumber > 1) { + movedToPreviousLine = true; lineNumber = lineNumber - 1; column = model.getLineMaxColumn(lineNumber); } @@ -171,29 +175,41 @@ export class WordOperations { let prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, column)); if (wordNavigationType === WordNavigationType.WordStart) { - if (prevWordOnLine && prevWordOnLine.wordType === WordType.Separator) { - if (prevWordOnLine.end - prevWordOnLine.start === 1 && prevWordOnLine.nextCharClass === WordCharacterClass.Regular) { - // Skip over a word made up of one single separator and followed by a regular character - prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1)); + + if (prevWordOnLine && !movedToPreviousLine) { + // Special case for Visual Studio compatibility: + // when starting in the trim whitespace at the end of a line, + // go to the end of the last word + const lastWhitespaceColumn = model.getLineLastNonWhitespaceColumn(lineNumber); + if (lastWhitespaceColumn < column) { + return new Position(lineNumber, prevWordOnLine.end + 1); } } - if (prevWordOnLine) { - column = prevWordOnLine.start + 1; - } else { - column = 1; - } - } else { - if (prevWordOnLine && column <= prevWordOnLine.end + 1) { - prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1)); - } - if (prevWordOnLine) { - column = prevWordOnLine.end + 1; - } else { - column = 1; - } + + return new Position(lineNumber, prevWordOnLine ? prevWordOnLine.start + 1 : 1); } - return new Position(lineNumber, column); + if (wordNavigationType === WordNavigationType.WordStartFast) { + if ( + prevWordOnLine + && prevWordOnLine.wordType === WordType.Separator + && prevWordOnLine.end - prevWordOnLine.start === 1 + && prevWordOnLine.nextCharClass === WordCharacterClass.Regular + ) { + // Skip over a word made up of one single separator and followed by a regular character + prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1)); + } + + return new Position(lineNumber, prevWordOnLine ? prevWordOnLine.start + 1 : 1); + } + + // We are stopping at the ending of words + + if (prevWordOnLine && column <= prevWordOnLine.end + 1) { + prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1)); + } + + return new Position(lineNumber, prevWordOnLine ? prevWordOnLine.end + 1 : 1); } public static moveWordRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { @@ -235,7 +251,7 @@ export class WordOperations { return new Position(lineNumber, column); } - private static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range { + protected static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range { const lineContent = model.getLineContent(position.lineNumber); const startIndex = position.column - 2; const lastNonWhitespace = strings.lastNonWhitespaceIndex(lineContent, startIndex); @@ -310,7 +326,7 @@ export class WordOperations { return len; } - private static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range { + protected static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range { const lineContent = model.getLineContent(position.lineNumber); const startIndex = position.column - 1; const firstNonWhitespace = this._findFirstNonWhitespaceChar(lineContent, startIndex); @@ -463,3 +479,176 @@ export class WordOperations { return cursor.move(true, lineNumber, column, 0); } } + +export function _lastWordPartEnd(str: string, startIndex: number = str.length - 1): number { + let ignoreUpperCase = !strings.isLowerAsciiLetter(str.charCodeAt(startIndex + 1)); + for (let i = startIndex; i >= 0; i--) { + const chCode = str.charCodeAt(i); + if (chCode === CharCode.Space || chCode === CharCode.Tab) { + if (i === 0) { + return 0; + } + const prevChCode = str.charCodeAt(i - 1); + if (prevChCode !== CharCode.Space && prevChCode !== CharCode.Tab) { + return i - 1; + } + } + if (!ignoreUpperCase && strings.isUpperAsciiLetter(chCode)) { + return i - 1; + } + if (ignoreUpperCase && i < startIndex && strings.isLowerAsciiLetter(chCode)) { + return i; + } + if (chCode === CharCode.Underline) { + if (i === 0) { + return 0; + } + const prevChCode = str.charCodeAt(i - 1); + if (prevChCode !== CharCode.Underline) { + return i - 1; + } + } + ignoreUpperCase = ignoreUpperCase && strings.isUpperAsciiLetter(chCode); + } + return -1; +} + +export function _nextWordPartBegin(str: string, startIndex: number = 0): number { + let prevChCode = str.charCodeAt(startIndex - 1); + let chCode = str.charCodeAt(startIndex); + // handle the special case ' X' and ' x' which is different from the standard methods + if ((prevChCode === CharCode.Space || prevChCode === CharCode.Tab) && (strings.isLowerAsciiLetter(chCode) || strings.isUpperAsciiLetter(chCode))) { + return startIndex + 1; + } + let ignoreUpperCase = strings.isUpperAsciiLetter(chCode); + for (let i = startIndex; i < str.length; ++i) { + chCode = str.charCodeAt(i); + if (chCode === CharCode.Space || chCode === CharCode.Tab) { + if (i + 1 >= str.length) { + return i + 1; + } + const nextChCode = str.charCodeAt(i + 1); + if (nextChCode !== CharCode.Space && nextChCode !== CharCode.Tab) { + return i + 2; + } + } + if (!ignoreUpperCase && strings.isUpperAsciiLetter(chCode)) { + return i + 1; + } + if (ignoreUpperCase && strings.isLowerAsciiLetter(chCode)) { + return i; // multiple UPPERCase : assume an upper case word and a CamelCase word - like DSLModel + } + ignoreUpperCase = ignoreUpperCase && strings.isUpperAsciiLetter(chCode); + if (chCode === CharCode.Underline) { + if (i + 1 >= str.length) { + return i + 1; + } + const nextChCode = str.charCodeAt(i + 1); + if (nextChCode !== CharCode.Underline) { + return i + 2; + } + } + } + return str.length + 1; +} + +export class WordPartOperations extends WordOperations { + public static deleteWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range { + if (!selection.isEmpty()) { + return selection; + } + + const position = new Position(selection.positionLineNumber, selection.positionColumn); + const lineNumber = position.lineNumber; + const column = position.column; + + if (lineNumber === 1 && column === 1) { + // Ignore deleting at beginning of file + return null; + } + + if (whitespaceHeuristics) { + let r = WordOperations._deleteWordLeftWhitespace(model, position); + if (r) { + return r; + } + } + + const wordRange = WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType); + const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(position.lineNumber), position.column - 2); + const wordPartRange = new Range(lineNumber, column, lineNumber, lastWordPartEnd + 2); + + if (wordPartRange.getStartPosition().isBeforeOrEqual(wordRange.getStartPosition())) { + return wordRange; + } + return wordPartRange; + } + + public static deleteWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range { + if (!selection.isEmpty()) { + return selection; + } + + const position = new Position(selection.positionLineNumber, selection.positionColumn); + const lineNumber = position.lineNumber; + const column = position.column; + + const lineCount = model.getLineCount(); + const maxColumn = model.getLineMaxColumn(lineNumber); + if (lineNumber === lineCount && column === maxColumn) { + // Ignore deleting at end of file + return null; + } + + if (whitespaceHeuristics) { + let r = WordOperations._deleteWordRightWhitespace(model, position); + if (r) { + return r; + } + } + + const wordRange = WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType); + const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(position.lineNumber), position.column); + const wordPartRange = new Range(lineNumber, column, lineNumber, nextWordPartBegin); + + if (wordRange.getEndPosition().isBeforeOrEqual(wordPartRange.getEndPosition())) { + return wordRange; + } + return wordPartRange; + } + + public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { + const lineNumber = position.lineNumber; + const column = position.column; + if (column === 1) { + return (lineNumber > 1 ? new Position(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)) : position); + } + + const wordPos = WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType); + const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(lineNumber), column - 2); + const wordPartPos = new Position(lineNumber, lastWordPartEnd + 2); + + if (wordPartPos.isBeforeOrEqual(wordPos)) { + return wordPos; + } + return wordPartPos; + } + + public static moveWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { + const lineNumber = position.lineNumber; + const column = position.column; + const maxColumn = model.getLineMaxColumn(lineNumber); + if (column === maxColumn) { + return (lineNumber < model.getLineCount() ? new Position(lineNumber + 1, 1) : position); + } + + const wordPos = WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType); + const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(lineNumber), column); + const wordPartPos = new Position(lineNumber, nextWordPartBegin); + + if (wordPos.isBeforeOrEqual(wordPartPos)) { + return wordPos; + } + return wordPartPos; + } +} diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index 9a8274f2326..2cd3132a952 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -255,7 +255,7 @@ class LineChange implements ILineChange { this.charChanges = charChanges; } - public static createFromDiffResult(shouldIgnoreTrimWhitespace: boolean, diffChange: IDiffChange, originalLineSequence: LineMarkerSequence, modifiedLineSequence: LineMarkerSequence, continueProcessingPredicate: () => boolean, shouldPostProcessCharChanges: boolean): LineChange { + public static createFromDiffResult(shouldIgnoreTrimWhitespace: boolean, diffChange: IDiffChange, originalLineSequence: LineMarkerSequence, modifiedLineSequence: LineMarkerSequence, continueProcessingPredicate: () => boolean, shouldComputeCharChanges: boolean, shouldPostProcessCharChanges: boolean): LineChange { let originalStartLineNumber: number; let originalEndLineNumber: number; let modifiedStartLineNumber: number; @@ -278,7 +278,7 @@ class LineChange implements ILineChange { modifiedEndLineNumber = modifiedLineSequence.getEndLineNumber(diffChange.modifiedStart + diffChange.modifiedLength - 1); } - if (diffChange.originalLength !== 0 && diffChange.modifiedLength !== 0 && continueProcessingPredicate()) { + if (shouldComputeCharChanges && diffChange.originalLength !== 0 && diffChange.modifiedLength !== 0 && continueProcessingPredicate()) { const originalCharSequence = originalLineSequence.getCharSequence(shouldIgnoreTrimWhitespace, diffChange.originalStart, diffChange.originalStart + diffChange.originalLength - 1); const modifiedCharSequence = modifiedLineSequence.getCharSequence(shouldIgnoreTrimWhitespace, diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength - 1); @@ -299,6 +299,7 @@ class LineChange implements ILineChange { } export interface IDiffComputerOpts { + shouldComputeCharChanges: boolean; shouldPostProcessCharChanges: boolean; shouldIgnoreTrimWhitespace: boolean; shouldMakePrettyDiff: boolean; @@ -306,6 +307,7 @@ export interface IDiffComputerOpts { export class DiffComputer { + private readonly shouldComputeCharChanges: boolean; private readonly shouldPostProcessCharChanges: boolean; private readonly shouldIgnoreTrimWhitespace: boolean; private readonly shouldMakePrettyDiff: boolean; @@ -318,6 +320,7 @@ export class DiffComputer { private computationStartTime: number; constructor(originalLines: string[], modifiedLines: string[], opts: IDiffComputerOpts) { + this.shouldComputeCharChanges = opts.shouldComputeCharChanges; this.shouldPostProcessCharChanges = opts.shouldPostProcessCharChanges; this.shouldIgnoreTrimWhitespace = opts.shouldIgnoreTrimWhitespace; this.shouldMakePrettyDiff = opts.shouldMakePrettyDiff; @@ -380,7 +383,7 @@ export class DiffComputer { if (this.shouldIgnoreTrimWhitespace) { let lineChanges: LineChange[] = []; for (let i = 0, length = rawChanges.length; i < length; i++) { - lineChanges.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, rawChanges[i], this.original, this.modified, this._continueProcessingPredicate.bind(this), this.shouldPostProcessCharChanges)); + lineChanges.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, rawChanges[i], this.original, this.modified, this._continueProcessingPredicate.bind(this), this.shouldComputeCharChanges, this.shouldPostProcessCharChanges)); } return lineChanges; } @@ -455,7 +458,7 @@ export class DiffComputer { if (nextChange) { // Emit the actual change - result.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, nextChange, this.original, this.modified, this._continueProcessingPredicate.bind(this), this.shouldPostProcessCharChanges)); + result.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, nextChange, this.original, this.modified, this._continueProcessingPredicate.bind(this), this.shouldComputeCharChanges, this.shouldPostProcessCharChanges)); originalLineIndex += nextChange.originalLength; modifiedLineIndex += nextChange.modifiedLength; @@ -475,15 +478,17 @@ export class DiffComputer { return; } + let charChanges: CharChange[]; + if (this.shouldComputeCharChanges) { + charChanges = [new CharChange( + originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, + modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn + )]; + } result.push(new LineChange( originalLineNumber, originalLineNumber, modifiedLineNumber, modifiedLineNumber, - [ - new CharChange( - originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, - modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn - ) - ] + charChanges )); } @@ -507,10 +512,12 @@ export class DiffComputer { if (prevChange.originalEndLineNumber + 1 === originalLineNumber && prevChange.modifiedEndLineNumber + 1 === modifiedLineNumber) { prevChange.originalEndLineNumber = originalLineNumber; prevChange.modifiedEndLineNumber = modifiedLineNumber; - prevChange.charChanges.push(new CharChange( - originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, - modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn - )); + if (this.shouldComputeCharChanges) { + prevChange.charChanges.push(new CharChange( + originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, + modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn + )); + } return true; } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 714b67fa3aa..99b03c2bf1c 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -5,7 +5,7 @@ 'use strict'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Position, IPosition } from 'vs/editor/common/core/position'; @@ -439,7 +439,7 @@ export interface IEditor { /** * Gets the current model attached to this editor. */ - getModel(): IEditorModel; + getModel(): IEditorModel | null; /** * Sets the current model attached to this editor. @@ -449,7 +449,7 @@ export interface IEditor { * will not be destroyed. * It is safe to call setModel(null) to simply detach the current model from the editor. */ - setModel(model: IEditorModel): void; + setModel(model: IEditorModel | null): void; /** * Change the decorations. All decorations added through this changeAccessor diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index e6cd1d9caf5..c306f76352c 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -30,6 +30,8 @@ export namespace EditorContextKeys { export const tabMovesFocus = new RawContextKey('editorTabMovesFocus', false); export const tabDoesNotMoveFocus: ContextKeyExpr = tabMovesFocus.toNegated(); export const isInEmbeddedEditor = new RawContextKey('isInEmbeddedEditor', undefined); + export const canUndo = new RawContextKey('canUndo', false); + export const canRedo = new RawContextKey('canRedo', false); // -- mode context keys export const languageId = new RawContextKey('editorLangId', undefined); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e4a6ba3feed..d24426a1f0b 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -5,7 +5,7 @@ 'use strict'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -733,33 +733,6 @@ export interface ITextModel { */ findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean): FindMatch; - /** - * Get the language associated with this model. - * @internal - */ - getLanguageIdentifier(): LanguageIdentifier; - - /** - * Get the language associated with this model. - */ - getModeId(): string; - - /** - * Get the word under or besides `position`. - * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. - * @return The word under or besides `position`. Might be null. - */ - getWordAtPosition(position: IPosition): IWordAtPosition; - - /** - * Get the word under or besides `position` trimmed to `position`.column - * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. - * @return The word under or besides `position`. Will never be null. - */ - getWordUntilPosition(position: IPosition): IWordAtPosition; - /** * Force tokenization information for `lineNumber` to be accurate. * @internal @@ -814,7 +787,6 @@ export interface ITextModel { /** * Get the word under or besides `position`. * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. * @return The word under or besides `position`. Might be null. */ getWordAtPosition(position: IPosition): IWordAtPosition; @@ -822,7 +794,6 @@ export interface ITextModel { /** * Get the word under or besides `position` trimmed to `position`.column * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. * @return The word under or besides `position`. Will never be null. */ getWordUntilPosition(position: IPosition): IWordAtPosition; @@ -1030,6 +1001,12 @@ export interface ITextModel { */ undo(): Selection[]; + /** + * Is there anything in the undo stack? + * @internal + */ + canUndo(): boolean; + /** * Redo edit operations until the next stop point created by `pushStackElement`. * The inverse edit operations will be pushed on the undo stack. @@ -1037,6 +1014,12 @@ export interface ITextModel { */ redo(): Selection[]; + /** + * Is there anything in the redo stack? + * @internal + */ + canRedo(): boolean; + /** * @deprecated Please use `onDidChangeContent` instead. * An event emitted when the contents of the model have changed. diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 13a955cf522..5f5e3066845 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -210,6 +210,10 @@ export class EditStack { return null; } + public canUndo(): boolean { + return (this.past.length > 0); + } + public redo(): IUndoRedoResult { if (this.future.length > 0) { @@ -233,4 +237,8 @@ export class EditStack { return null; } + + public canRedo(): boolean { + return (this.future.length > 0); + } } diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index ca11c6f9cb4..28839e413af 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -12,13 +12,14 @@ import { IModelDecoration, TrackedRangeStickiness as ActualTrackedRangeStickines // The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. // -export const ClassName = { - EditorHintDecoration: 'squiggly-hint', - EditorInfoDecoration: 'squiggly-info', - EditorWarningDecoration: 'squiggly-warning', - EditorErrorDecoration: 'squiggly-error', - EditorUnnecessaryDecoration: 'squiggly-overlay-unnecessary' -}; +export const enum ClassName { + EditorHintDecoration = 'squiggly-hint', + EditorInfoDecoration = 'squiggly-info', + EditorWarningDecoration = 'squiggly-warning', + EditorErrorDecoration = 'squiggly-error', + EditorUnnecessaryDecoration = 'squiggly-unnecessary', + EditorUnnecessaryInlineDecoration = 'squiggly-inline-unnecessary' +} /** * Describes the behavior of decorations when typing/editing near their edges. diff --git a/src/vs/editor/common/model/mirrorTextModel.ts b/src/vs/editor/common/model/mirrorTextModel.ts index 7aa8eac9918..744ab95fabf 100644 --- a/src/vs/editor/common/model/mirrorTextModel.ts +++ b/src/vs/editor/common/model/mirrorTextModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { IModelContentChange } from 'vs/editor/common/model/textModelEvents'; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 491f5a38134..4d4b03519d1 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -1100,7 +1100,7 @@ export class PieceTreeBase { let endColumn = endOffset - this._buffers[0].lineStarts[endIndex]; let endPos = { line: endIndex, column: endColumn }; let newPiece = new Piece( - 0, /** todo */ + 0, /** todo@peng */ start, endPos, this.getLineFeedCnt(0, start, endPos), diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 53dc47f82e1..dcc3bdc2e4b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import * as model from 'vs/editor/common/model'; import { LanguageIdentifier, TokenizationRegistry, LanguageId } from 'vs/editor/common/modes'; @@ -46,7 +46,7 @@ export function createTextBufferFactory(text: string): model.ITextBufferFactory } export function createTextBufferFactoryFromStream(stream: IStringStream, filter?: (chunk: string) => string): TPromise { - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { let done = false; let builder = createTextBufferBuilder(); @@ -1323,7 +1323,12 @@ export class TextModel extends Disposable implements model.ITextModel { for (let i = 0, len = contentChanges.length; i < len; i++) { const change = contentChanges[i]; const [eolCount, firstLineLength] = TextModel._eolCount(change.text); - this._tokens.applyEdits(change.range, eolCount, firstLineLength); + try { + this._tokens.applyEdits(change.range, eolCount, firstLineLength); + } catch (err) { + // emergency recovery => reset tokens + this._tokens = new ModelLinesTokens(this._tokens.languageIdentifier, this._tokens.tokenizationSupport); + } this._onDidChangeDecorations.fire(); this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers); @@ -1416,6 +1421,10 @@ export class TextModel extends Disposable implements model.ITextModel { } } + public canUndo(): boolean { + return this._commandManager.canUndo(); + } + private _redo(): Selection[] { this._isRedoing = true; let r = this._commandManager.redo(); @@ -1441,6 +1450,10 @@ export class TextModel extends Disposable implements model.ITextModel { } } + public canRedo(): boolean { + return this._commandManager.canRedo(); + } + //#endregion //#region Decorations diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 2aa69522f6b..23a2b611dc9 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; +import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; -import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token'; -import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Position } from 'vs/editor/common/core/position'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { Event } from 'vs/base/common/event'; -import { TokenizationRegistryImpl } from 'vs/editor/common/modes/tokenizationRegistry'; -import { Color } from 'vs/base/common/color'; -import { IMarkerData } from 'vs/platform/markers/common/markers'; -import * as model from 'vs/editor/common/model'; import { isObject } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token'; +import * as model from 'vs/editor/common/model'; +import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; +import { TokenizationRegistryImpl } from 'vs/editor/common/modes/tokenizationRegistry'; +import { IMarkerData } from 'vs/platform/markers/common/markers'; /** * Open ended enum at runtime @@ -216,6 +216,14 @@ export interface IState { equals(other: IState): boolean; } +/** + * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves + * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a + * thenable. + */ +export type ProviderResult = T | undefined | null | Thenable; + /** * A hover represents additional information for a symbol or word. Hovers are * rendered in a tooltip-like widget. @@ -244,7 +252,7 @@ export interface HoverProvider { * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: model.ITextModel, position: Position, token: CancellationToken): Hover | Thenable; + provideHover(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -293,6 +301,7 @@ export interface ISuggestion { documentation?: string | IMarkdownString; filterText?: string; sortText?: string; + preselect?: boolean; noAutoAccept?: boolean; commitCharacters?: string[]; overwriteBefore?: number; @@ -300,6 +309,7 @@ export interface ISuggestion { additionalTextEdits?: model.ISingleEditOperation[]; command?: Command; snippetType?: SnippetType; + noWhitespaceAdjust?: boolean; } /** @@ -335,9 +345,9 @@ export interface ISuggestSupport { triggerCharacters?: string[]; - provideCompletionItems(model: model.ITextModel, position: Position, context: SuggestContext, token: CancellationToken): ISuggestResult | Thenable; + provideCompletionItems(model: model.ITextModel, position: Position, context: SuggestContext, token: CancellationToken): ProviderResult; - resolveCompletionItem?(model: model.ITextModel, position: Position, item: ISuggestion, token: CancellationToken): ISuggestion | Thenable; + resolveCompletionItem?(model: model.ITextModel, position: Position, item: ISuggestion, token: CancellationToken): ProviderResult; } export interface CodeAction { @@ -351,7 +361,7 @@ export interface CodeAction { /** * @internal */ -export enum CodeActionTrigger { +export const enum CodeActionTrigger { Automatic = 1, Manual = 2, } @@ -373,12 +383,12 @@ export interface CodeActionProvider { /** * Provide commands for the given document and range. */ - provideCodeActions(model: model.ITextModel, range: Range | Selection, context: CodeActionContext, token: CancellationToken): CodeAction[] | Thenable; + provideCodeActions(model: model.ITextModel, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult; /** * Optional list of of CodeActionKinds that this provider returns. */ - providedCodeActionKinds?: string[]; + providedCodeActionKinds?: ReadonlyArray; } /** @@ -437,6 +447,18 @@ export interface SignatureHelp { */ activeParameter: number; } + +export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3, +} + +export interface SignatureHelpContext { + triggerReason: SignatureHelpTriggerReason; + triggerCharacter?: string; +} + /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. @@ -448,7 +470,7 @@ export interface SignatureHelpProvider { /** * Provide help for the signature at the given position and document. */ - provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable; + provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; } /** @@ -492,7 +514,7 @@ export interface DocumentHighlightProvider { * Provide a set of document highlights, like all occurrences of a variable or * all exit-points of a function. */ - provideDocumentHighlights(model: model.ITextModel, position: Position, token: CancellationToken): DocumentHighlight[] | Thenable; + provideDocumentHighlights(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -513,7 +535,7 @@ export interface ReferenceProvider { /** * Provide a set of project-wide references for the given position and document. */ - provideReferences(model: model.ITextModel, position: Position, context: ReferenceContext, token: CancellationToken): Location[] | Thenable; + provideReferences(model: model.ITextModel, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult; } /** @@ -537,6 +559,13 @@ export interface Location { */ export type Definition = Location | Location[]; +export interface DefinitionLink { + origin?: IRange; + uri: URI; + range: IRange; + selectionRange?: IRange; +} + /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) @@ -546,7 +575,7 @@ export interface DefinitionProvider { /** * Provide the definition of the symbol at the given position and document. */ - provideDefinition(model: model.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideDefinition(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -557,7 +586,7 @@ export interface ImplementationProvider { /** * Provide the implementation of the symbol at the given position and document. */ - provideImplementation(model: model.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideImplementation(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -568,7 +597,7 @@ export interface TypeDefinitionProvider { /** * Provide the type definition of the symbol at the given position and document. */ - provideTypeDefinition(model: model.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideTypeDefinition(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -638,60 +667,32 @@ export const symbolKindToCssClass = (function () { _fromMapping[SymbolKind.TypeParameter] = 'type-parameter'; return function toCssClassName(kind: SymbolKind): string { - return _fromMapping[kind] || 'property'; + return `symbol-icon ${_fromMapping[kind] || 'property'}`; }; })(); -/** - * @internal - */ -export interface IOutline { - entries: SymbolInformation[]; -} -/** - * Represents information about programming constructs like variables, classes, - * interfaces etc. - */ -export interface SymbolInformation { - /** - * The name of this symbol. - */ +export interface DocumentSymbol { name: string; - /** - * The detail of this symbol. - */ - detail?: string; - /** - * The name of the symbol containing this symbol. - */ - containerName?: string; - /** - * The kind of this symbol. - */ + detail: string; kind: SymbolKind; - /** - * The location of this symbol. - */ - location: Location; - /** - * The defining range of this symbol. - */ - definingRange: IRange; - - children?: SymbolInformation[]; + containerName?: string; + range: IRange; + selectionRange: IRange; + children?: DocumentSymbol[]; } + /** * The document symbol provider interface defines the contract between extensions and * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_goto-symbol)-feature. */ export interface DocumentSymbolProvider { - extensionId?: string; + displayName?: string; /** * Provide symbol information for the given document. */ - provideDocumentSymbols(model: model.ITextModel, token: CancellationToken): SymbolInformation[] | Thenable; + provideDocumentSymbols(model: model.ITextModel, token: CancellationToken): ProviderResult; } export interface TextEdit { @@ -721,7 +722,7 @@ export interface DocumentFormattingEditProvider { /** * Provide formatting edits for a whole document. */ - provideDocumentFormattingEdits(model: model.ITextModel, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideDocumentFormattingEdits(model: model.ITextModel, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** * The document formatting provider interface defines the contract between extensions and @@ -735,7 +736,7 @@ export interface DocumentRangeFormattingEditProvider { * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. */ - provideDocumentRangeFormattingEdits(model: model.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideDocumentRangeFormattingEdits(model: model.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** * The document formatting provider interface defines the contract between extensions and @@ -750,7 +751,7 @@ export interface OnTypeFormattingEditProvider { * what range the position to expand to, like find the matching `{` * when `}` has been entered. */ - provideOnTypeFormattingEdits(model: model.ITextModel, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideOnTypeFormattingEdits(model: model.ITextModel, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** @@ -772,8 +773,8 @@ export interface ILink { * A provider of links. */ export interface LinkProvider { - provideLinks(model: model.ITextModel, token: CancellationToken): ILink[] | Thenable; - resolveLink?: (link: ILink, token: CancellationToken) => ILink | Thenable; + provideLinks(model: model.ITextModel, token: CancellationToken): ProviderResult; + resolveLink?: (link: ILink, token: CancellationToken) => ProviderResult; } /** @@ -847,11 +848,11 @@ export interface DocumentColorProvider { /** * Provides the color ranges for a specific model. */ - provideDocumentColors(model: model.ITextModel, token: CancellationToken): IColorInformation[] | Thenable; + provideDocumentColors(model: model.ITextModel, token: CancellationToken): ProviderResult; /** * Provide the string representations for a color. */ - provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; + provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): ProviderResult; } export interface FoldingContext { } @@ -862,18 +863,18 @@ export interface FoldingRangeProvider { /** * Provides the color ranges for a specific model. */ - provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): FoldingRange[] | Thenable; + provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): ProviderResult; } export interface FoldingRange { /** - * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * The one-based start line of the range to fold. The folded area starts after the line's last character. */ start: number; /** - * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * The one-based end line of the range to fold. The folded area ends with the line's last character. */ end: number; @@ -926,6 +927,7 @@ export function isResourceTextEdit(thing: any): thing is ResourceTextEdit { export interface ResourceFileEdit { oldUri: URI; newUri: URI; + options: { overwrite?: boolean, ignoreIfNotExists?: boolean, ignoreIfExists?: boolean, recursive?: boolean }; } export interface ResourceTextEdit { @@ -936,17 +938,19 @@ export interface ResourceTextEdit { export interface WorkspaceEdit { edits: Array; - rejectReason?: string; // TODO@joh, move to rename } +export interface Rejection { + rejectReason?: string; +} export interface RenameLocation { range: IRange; text: string; } export interface RenameProvider { - provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; - resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): RenameLocation | Thenable; + provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): ProviderResult; + resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } @@ -957,6 +961,9 @@ export interface Command { arguments?: any[]; } +/** + * @internal + */ export interface CommentInfo { owner: number; threads: CommentThread[]; @@ -964,6 +971,9 @@ export interface CommentInfo { reply?: Command; } +/** + * @internal + */ export enum CommentThreadCollapsibleState { /** * Determines an item is collapsed @@ -975,6 +985,9 @@ export enum CommentThreadCollapsibleState { Expanded = 1 } +/** + * @internal + */ export interface CommentThread { threadId: string; resource: string; @@ -984,11 +997,17 @@ export interface CommentThread { reply?: Command; } +/** + * @internal + */ export interface NewCommentAction { ranges: IRange[]; actions: Command[]; } +/** + * @internal + */ export interface Comment { readonly commentId: string; readonly body: IMarkdownString; @@ -997,6 +1016,9 @@ export interface Comment { readonly command?: Command; } +/** + * @internal + */ export interface CommentThreadChangedEvent { readonly owner: number; /** @@ -1015,7 +1037,9 @@ export interface CommentThreadChangedEvent { readonly changed: CommentThread[]; } - +/** + * @internal + */ export interface DocumentCommentProvider { provideDocumentComments(resource: URI, token: CancellationToken): Promise; createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise; @@ -1023,7 +1047,9 @@ export interface DocumentCommentProvider { onDidChangeCommentThreads(): Event; } - +/** + * @internal + */ export interface WorkspaceCommentProvider { provideWorkspaceComments(token: CancellationToken): Promise; createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise; @@ -1038,8 +1064,8 @@ export interface ICodeLensSymbol { } export interface CodeLensProvider { onDidChange?: Event; - provideCodeLenses(model: model.ITextModel, token: CancellationToken): ICodeLensSymbol[] | Thenable; - resolveCodeLens?(model: model.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ICodeLensSymbol | Thenable; + provideCodeLenses(model: model.ITextModel, token: CancellationToken): ProviderResult; + resolveCodeLens?(model: model.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ProviderResult; } // --- feature registries ------ diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index 410d5694d01..d0f6b77d2b8 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -62,6 +62,13 @@ export interface LanguageConfiguration { */ surroundingPairs?: IAutoClosingPair[]; + /** + * Defines what characters must be after the cursor for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. + * + * This is typically the set of characters which can not start an expression, such as whitespace, closing brackets, non-unary operators, etc. + */ + autoCloseBefore?: string; + /** * The language's folding rules. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index a184362fd69..6dccea67963 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -13,7 +13,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { createScopedLineTokens } from 'vs/editor/common/modes/supports'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; @@ -119,6 +119,7 @@ export class RichEditSupport { onEnterRules: (prev ? current.onEnterRules || prev.onEnterRules : current.onEnterRules), autoClosingPairs: (prev ? current.autoClosingPairs || prev.autoClosingPairs : current.autoClosingPairs), surroundingPairs: (prev ? current.surroundingPairs || prev.surroundingPairs : current.surroundingPairs), + autoCloseBefore: (prev ? current.autoCloseBefore || prev.autoCloseBefore : current.autoCloseBefore), folding: (prev ? current.folding || prev.folding : current.folding), __electricCharacterSupport: (prev ? current.__electricCharacterSupport || prev.__electricCharacterSupport : current.__electricCharacterSupport), }; @@ -189,14 +190,12 @@ export class LanguageConfigurationRegistryImpl { let current = new RichEditSupport(languageIdentifier, previous, configuration); this._entries[languageIdentifier.id] = current; this._onDidChange.fire({ languageIdentifier }); - return { - dispose: () => { - if (this._entries[languageIdentifier.id] === current) { - this._entries[languageIdentifier.id] = previous; - this._onDidChange.fire({ languageIdentifier }); - } + return toDisposable(() => { + if (this._entries[languageIdentifier.id] === current) { + this._entries[languageIdentifier.id] = previous; + this._onDidChange.fire({ languageIdentifier }); } - }; + }); } private _getRichEditSupport(languageId: LanguageId): RichEditSupport { @@ -271,6 +270,14 @@ export class LanguageConfigurationRegistryImpl { return characterPairSupport.getAutoClosingPairs(); } + public getAutoCloseBeforeSet(languageId: LanguageId): string { + let characterPairSupport = this._getCharacterPairSupport(languageId); + if (!characterPairSupport) { + return CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED; + } + return characterPairSupport.getAutoCloseBeforeSet(); + } + public getSurroundingPairs(languageId: LanguageId): IAutoClosingPair[] { let characterPairSupport = this._getCharacterPairSupport(languageId); if (!characterPairSupport) { diff --git a/src/vs/editor/common/modes/languageFeatureRegistry.ts b/src/vs/editor/common/modes/languageFeatureRegistry.ts index c5e4e7010d8..9dfd73755f2 100644 --- a/src/vs/editor/common/modes/languageFeatureRegistry.ts +++ b/src/vs/editor/common/modes/languageFeatureRegistry.ts @@ -6,7 +6,7 @@ 'use strict'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { LanguageSelector, score } from 'vs/editor/common/modes/languageSelector'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; @@ -54,19 +54,17 @@ export default class LanguageFeatureRegistry { this._lastCandidate = undefined; this._onDidChange.fire(this._entries.length); - return { - dispose: () => { - if (entry) { - let idx = this._entries.indexOf(entry); - if (idx >= 0) { - this._entries.splice(idx, 1); - this._lastCandidate = undefined; - this._onDidChange.fire(this._entries.length); - entry = undefined; - } + return toDisposable(() => { + if (entry) { + let idx = this._entries.indexOf(entry); + if (idx >= 0) { + this._entries.splice(idx, 1); + this._lastCandidate = undefined; + this._onDidChange.fire(this._entries.length); + entry = undefined; } } - }; + }); } has(model: ITextModel): boolean { diff --git a/src/vs/editor/common/modes/languageSelector.ts b/src/vs/editor/common/modes/languageSelector.ts index 513ca2cc2d6..c572fe8d78f 100644 --- a/src/vs/editor/common/modes/languageSelector.ts +++ b/src/vs/editor/common/modes/languageSelector.ts @@ -5,7 +5,7 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { match as matchGlobPattern, IRelativePattern } from 'vs/base/common/glob'; // TODO@Alex export interface LanguageFilter { diff --git a/src/vs/editor/common/modes/supports/characterPair.ts b/src/vs/editor/common/modes/supports/characterPair.ts index b6ce3dc3f35..70eb9ed7b65 100644 --- a/src/vs/editor/common/modes/supports/characterPair.ts +++ b/src/vs/editor/common/modes/supports/characterPair.ts @@ -9,10 +9,14 @@ import { CharacterPair, IAutoClosingPair, IAutoClosingPairConditional, StandardA export class CharacterPairSupport { + static readonly DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED = ';:.,=}])> \n\t'; + static readonly DEFAULT_AUTOCLOSE_BEFORE_WHITESPACE = ' \n\t'; + private readonly _autoClosingPairs: StandardAutoClosingPairConditional[]; private readonly _surroundingPairs: IAutoClosingPair[]; + private readonly _autoCloseBefore: string; - constructor(config: { brackets?: CharacterPair[]; autoClosingPairs?: IAutoClosingPairConditional[], surroundingPairs?: IAutoClosingPair[] }) { + constructor(config: { brackets?: CharacterPair[]; autoClosingPairs?: IAutoClosingPairConditional[], surroundingPairs?: IAutoClosingPair[], autoCloseBefore?: string }) { if (config.autoClosingPairs) { this._autoClosingPairs = config.autoClosingPairs.map(el => new StandardAutoClosingPairConditional(el)); } else if (config.brackets) { @@ -21,6 +25,8 @@ export class CharacterPairSupport { this._autoClosingPairs = []; } + this._autoCloseBefore = typeof config.autoCloseBefore === 'string' ? config.autoCloseBefore : CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED; + this._surroundingPairs = config.surroundingPairs || this._autoClosingPairs; } @@ -28,6 +34,10 @@ export class CharacterPairSupport { return this._autoClosingPairs; } + public getAutoCloseBeforeSet(): string { + return this._autoCloseBefore; + } + public shouldAutoClosePair(character: string, context: ScopedLineTokens, column: number): boolean { // Always complete on empty line if (context.getTokenCount() === 0) { diff --git a/src/vs/editor/common/modes/supports/electricCharacter.ts b/src/vs/editor/common/modes/supports/electricCharacter.ts index 085a2aa6959..715a3b06840 100644 --- a/src/vs/editor/common/modes/supports/electricCharacter.ts +++ b/src/vs/editor/common/modes/supports/electricCharacter.ts @@ -121,7 +121,8 @@ export class BracketElectricCharacterSupport { } // check if the full open bracket matches - let actual = line.substring(line.length - pair.open.length + 1) + character; + let start = column - pair.open.length + 1; + let actual = line.substring(start - 1, column - 1) + character; if (actual !== pair.open) { continue; } diff --git a/src/vs/editor/common/modes/supports/tokenization.ts b/src/vs/editor/common/modes/supports/tokenization.ts index 85685d0ff41..db3d8400dcf 100644 --- a/src/vs/editor/common/modes/supports/tokenization.ts +++ b/src/vs/editor/common/modes/supports/tokenization.ts @@ -99,7 +99,7 @@ export function parseTokenTheme(source: ITokenThemeRule[]): ParsedTokenThemeRule /** * Resolve rules (i.e. inheritance). */ -function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[]): TokenTheme { +function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme { // Sort rules lexicographically, and then by index if necessary parsedThemeRules.sort((a, b) => { @@ -127,9 +127,17 @@ function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[]): } } let colorMap = new ColorMap(); - // ensure default foreground gets id 1 and default background gets id 2 - let defaults = new ThemeTrieElementRule(defaultFontStyle, colorMap.getId(defaultForeground), colorMap.getId(defaultBackground)); + // start with token colors from custom token themes + for (let color of customTokenColors) { + colorMap.getId(color); + } + + + let foregroundColorId = colorMap.getId(defaultForeground); + let backgroundColorId = colorMap.getId(defaultBackground); + + let defaults = new ThemeTrieElementRule(defaultFontStyle, foregroundColorId, backgroundColorId); let root = new ThemeTrieElement(defaults); for (let i = 0, len = parsedThemeRules.length; i < len; i++) { let rule = parsedThemeRules[i]; @@ -139,6 +147,8 @@ function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[]): return new TokenTheme(colorMap, root); } +const colorRegExp = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/; + export class ColorMap { private _lastColorId: number; @@ -155,10 +165,11 @@ export class ColorMap { if (color === null) { return 0; } - color = color.toUpperCase(); - if (!/^[0-9A-F]{6}$/.test(color)) { - throw new Error('Illegal color name: ' + color); + const match = color.match(colorRegExp); + if (!match) { + throw new Error('Illegal value for token color: ' + color); } + color = match[1].toUpperCase(); let value = this._color2id.get(color); if (value) { return value; @@ -177,12 +188,12 @@ export class ColorMap { export class TokenTheme { - public static createFromRawTokenTheme(source: ITokenThemeRule[]): TokenTheme { - return this.createFromParsedTokenTheme(parseTokenTheme(source)); + public static createFromRawTokenTheme(source: ITokenThemeRule[], customTokenColors: string[]): TokenTheme { + return this.createFromParsedTokenTheme(parseTokenTheme(source), customTokenColors); } - public static createFromParsedTokenTheme(source: ParsedTokenThemeRule[]): TokenTheme { - return resolveParsedTokenThemeRules(source); + public static createFromParsedTokenTheme(source: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme { + return resolveParsedTokenThemeRules(source, customTokenColors); } private readonly _colorMap: ColorMap; diff --git a/src/vs/editor/common/modes/tokenizationRegistry.ts b/src/vs/editor/common/modes/tokenizationRegistry.ts index 660ef056881..dd13f213e76 100644 --- a/src/vs/editor/common/modes/tokenizationRegistry.ts +++ b/src/vs/editor/common/modes/tokenizationRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes'; import { Color } from 'vs/base/common/color'; @@ -33,15 +33,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { public register(language: string, support: ITokenizationSupport): IDisposable { this._map[language] = support; this.fire([language]); - return { - dispose: () => { - if (this._map[language] !== support) { - return; - } - delete this._map[language]; - this.fire([language]); + return toDisposable(() => { + if (this._map[language] !== support) { + return; } - }; + delete this._map[language]; + this.fire([language]); + }); } public get(language: string): ITokenizationSupport { diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index bf4848670db..552ab3c135f 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -5,7 +5,7 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; @@ -16,13 +16,14 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { MirrorTextModel as BaseMirrorModel, IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { IInplaceReplaceSupportResult, ILink, ISuggestResult, ISuggestion, TextEdit } from 'vs/editor/common/modes'; -import { computeLinks } from 'vs/editor/common/modes/linkComputer'; +import { computeLinks, ILinkComputerTarget } from 'vs/editor/common/modes/linkComputer'; import { BasicInplaceReplace } from 'vs/editor/common/modes/supports/inplaceReplaceSupport'; import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { IWordAtPosition, EndOfLineSequence } from 'vs/editor/common/model'; import { globals } from 'vs/base/common/platform'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; +import { mergeSort } from 'vs/base/common/arrays'; export interface IMirrorModel { readonly uri: URI; @@ -50,7 +51,7 @@ export interface IRawModelData { /** * @internal */ -export interface ICommonModel { +export interface ICommonModel extends ILinkComputerTarget, IMirrorModel { uri: URI; version: number; eol: string; @@ -59,7 +60,7 @@ export interface ICommonModel { getLinesContent(): string[]; getLineCount(): number; getLineContent(lineNumber: number): string; - createWordIterator(wordDefinition: RegExp): IIterator; + createWordIterator(wordDefinition: RegExp): Iterator; getWordUntilPosition(position: IPosition, wordDefinition: RegExp): IWordAtPosition; getValueInRange(range: IRange): string; getWordAtPosition(position: IPosition, wordDefinition: RegExp): Range; @@ -147,7 +148,7 @@ class MirrorModel extends BaseMirrorModel implements ICommonModel { }; } - public createWordIterator(wordDefinition: RegExp): IIterator { + public createWordIterator(wordDefinition: RegExp): Iterator { let obj = { done: false, value: '' @@ -332,6 +333,7 @@ export abstract class BaseEditorSimpleWorker { let originalLines = original.getLinesContent(); let modifiedLines = modified.getLinesContent(); let diffComputer = new DiffComputer(originalLines, modifiedLines, { + shouldComputeCharChanges: true, shouldPostProcessCharChanges: true, shouldIgnoreTrimWhitespace: ignoreTrimWhitespace, shouldMakePrettyDiff: true @@ -349,6 +351,7 @@ export abstract class BaseEditorSimpleWorker { let originalLines = original.getLinesContent(); let modifiedLines = modified.getLinesContent(); let diffComputer = new DiffComputer(originalLines, modifiedLines, { + shouldComputeCharChanges: false, shouldPostProcessCharChanges: false, shouldIgnoreTrimWhitespace: ignoreTrimWhitespace, shouldMakePrettyDiff: true @@ -372,6 +375,8 @@ export abstract class BaseEditorSimpleWorker { const result: TextEdit[] = []; let lastEol: EndOfLineSequence; + edits = mergeSort(edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + for (let { range, text, eol } of edits) { if (typeof eol === 'number') { @@ -527,6 +532,7 @@ export abstract class BaseEditorSimpleWorker { } return TPromise.as(methods); } + // ESM-comment-begin return new TPromise((c, e) => { require([moduleId], (foreignModule: { create: IForeignModuleFactory }) => { this._foreignModule = foreignModule.create(ctx, createData); @@ -542,6 +548,11 @@ export abstract class BaseEditorSimpleWorker { }, e); }); + // ESM-comment-end + + // ESM-uncomment-begin + // return TPromise.wrapError(new Error(`Unexpected usage`)); + // ESM-uncomment-end } // foreign method request diff --git a/src/vs/editor/common/services/editorWorkerService.ts b/src/vs/editor/common/services/editorWorkerService.ts index dd44f4562e8..4c2a7a6464b 100644 --- a/src/vs/editor/common/services/editorWorkerService.ts +++ b/src/vs/editor/common/services/editorWorkerService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChange, ILineChange } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index efbf104f703..8078efd8abe 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IntervalTimer, ShallowCancelThenPromise, wireCancellationToken } from 'vs/base/common/async'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { IntervalTimer } from 'vs/base/common/async'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { SimpleWorkerClient, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker'; import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; @@ -63,7 +63,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker if (!canSyncModel(this._modelService, model.uri)) { return TPromise.as([]); // File too large } - return wireCancellationToken(token, this._workerManager.withWorker().then(client => client.computeLinks(model.uri))); + return this._workerManager.withWorker().then(client => client.computeLinks(model.uri)); } })); this._register(modes.SuggestRegistry.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService))); @@ -285,11 +285,9 @@ class EditorModelManager extends Disposable { toDispose.push(model.onWillDispose(() => { this._stopModelSync(modelUrl); })); - toDispose.push({ - dispose: () => { - this._proxy.acceptRemovedModel(modelUrl); - } - }); + toDispose.push(toDisposable(() => { + this._proxy.acceptRemovedModel(modelUrl); + })); this._syncedModels[modelUrl] = toDispose; } @@ -323,7 +321,7 @@ class SynchronousWorkerClient implements IWorkerClient } public getProxyObject(): TPromise { - return new ShallowCancelThenPromise(this._proxyObj); + return this._proxyObj; } } @@ -358,11 +356,11 @@ export class EditorWorkerClient extends Disposable { } protected _getProxy(): TPromise { - return new ShallowCancelThenPromise(this._getOrCreateWorker().getProxyObject().then(null, (err) => { + return this._getOrCreateWorker().getProxyObject().then(null, (err) => { logOnceWebWorkerWarning(err); this._worker = new SynchronousWorkerClient(new EditorSimpleWorkerImpl(null)); return this._getOrCreateWorker().getProxyObject(); - })); + }); } private _getOrCreateModelManager(proxy: EditorSimpleWorkerImpl): EditorModelManager { diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index 13225e51b8c..8618d2a2458 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -13,6 +13,7 @@ import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService'; import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; import { NULL_MODE_ID, NULL_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/nullMode'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { URI } from 'vs/base/common/uri'; const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -23,7 +24,7 @@ export interface IResolvedLanguage { aliases: string[]; extensions: string[]; filenames: string[]; - configurationFiles: string[]; + configurationFiles: URI[]; } export class LanguagesRegistry { @@ -188,7 +189,7 @@ export class LanguagesRegistry { } } - if (typeof lang.configuration === 'string') { + if (lang.configuration) { resolvedLanguage.configurationFiles.push(lang.configuration); } } @@ -224,7 +225,7 @@ export class LanguagesRegistry { return this._lowercaseNameMap[languageNameLower].language; } - public getConfigurationFiles(modeId: string): string[] { + public getConfigurationFiles(modeId: string): URI[] { if (!hasOwnProperty.call(this._languages, modeId)) { return []; } diff --git a/src/vs/editor/common/services/modeService.ts b/src/vs/editor/common/services/modeService.ts index e420aa63096..b042208a197 100644 --- a/src/vs/editor/common/services/modeService.ts +++ b/src/vs/editor/common/services/modeService.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; +import { URI } from 'vs/base/common/uri'; export const IModeService = createDecorator('modeService'); @@ -19,18 +20,7 @@ export interface ILanguageExtensionPoint { firstLine?: string; aliases?: string[]; mimetypes?: string[]; - configuration?: string; -} - -export interface IValidLanguageExtensionPoint { - id: string; - extensions: string[]; - filenames: string[]; - filenamePatterns: string[]; - firstLine: string; - aliases: string[]; - mimetypes: string[]; - configuration: string; + configuration?: URI; } export interface IModeService { @@ -50,7 +40,7 @@ export interface IModeService { getModeIdByFilenameOrFirstLine(filename: string, firstLine?: string): string; getModeId(commaSeparatedMimetypesOrCommaSeparatedIds: string): string; getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier; - getConfigurationFiles(modeId: string): string[]; + getConfigurationFiles(modeId: string): URI[]; // --- instantiation getMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): IMode; diff --git a/src/vs/editor/common/services/modeServiceImpl.ts b/src/vs/editor/common/services/modeServiceImpl.ts index 9c7b5b67680..edce40eaacb 100644 --- a/src/vs/editor/common/services/modeServiceImpl.ts +++ b/src/vs/editor/common/services/modeServiceImpl.ts @@ -11,6 +11,7 @@ import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; import { FrankensteinMode } from 'vs/editor/common/modes/abstractMode'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { URI } from 'vs/base/common/uri'; export class ModeServiceImpl implements IModeService { public _serviceBrand: any; @@ -87,7 +88,7 @@ export class ModeServiceImpl implements IModeService { return this._registry.getLanguageIdentifier(modeId); } - public getConfigurationFiles(modeId: string): string[] { + public getConfigurationFiles(modeId: string): URI[] { return this._registry.getConfigurationFiles(modeId); } @@ -109,7 +110,7 @@ export class ModeServiceImpl implements IModeService { let r: IMode = null; this.getOrCreateMode(commaSeparatedMimetypesOrCommaSeparatedIds).then((mode) => { r = mode; - }).done(null, onUnexpectedError); + }, onUnexpectedError); return r; } return null; diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 6db067c4b04..b487d9ae45b 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -5,7 +5,7 @@ 'use strict'; import { Event } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel, ITextModelCreationOptions, ITextBufferFactory } from 'vs/editor/common/model'; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 69619294284..221165f6944 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -9,7 +9,7 @@ import * as network from 'vs/base/common/network'; import { Event, Emitter } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IMarker, IMarkerService, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; import { Range } from 'vs/editor/common/core/range'; @@ -28,6 +28,7 @@ import { overviewRulerWarning, overviewRulerError, overviewRulerInfo } from 'vs/ import { ITextModel, IModelDeltaDecoration, IModelDecorationOptions, TrackedRangeStickiness, OverviewRulerLane, DefaultEndOfLine, ITextModelCreationOptions, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBufferFactory, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { basename } from 'vs/base/common/paths'; +import { isThenable } from 'vs/base/common/async'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -73,8 +74,8 @@ class ModelMarkerHandler { let newModelDecorations: IModelDeltaDecoration[] = markers.map((marker) => { return { - range: this._createDecorationRange(modelData.model, marker), - options: this._createDecorationOption(marker) + range: ModelMarkerHandler._createDecorationRange(modelData.model, marker), + options: ModelMarkerHandler._createDecorationOption(marker) }; }); @@ -85,9 +86,12 @@ class ModelMarkerHandler { let ret = Range.lift(rawMarker); - if (rawMarker.severity === MarkerSeverity.Hint && Range.spansMultipleLines(ret)) { - // never render hints on multiple lines - ret = ret.setEndPosition(ret.startLineNumber, ret.startColumn); + if (rawMarker.severity === MarkerSeverity.Hint) { + if (!rawMarker.tags || rawMarker.tags.indexOf(MarkerTag.Unnecessary) === -1) { + // * never render hints on multiple lines + // * make enough space for three dots + ret = ret.setEndPosition(ret.startLineNumber, ret.startColumn + 2); + } } ret = model.validateRange(ret); @@ -131,7 +135,9 @@ class ModelMarkerHandler { switch (marker.severity) { case MarkerSeverity.Hint: - if (!marker.customTags || marker.customTags.indexOf(MarkerTag.Unnecessary) === -1) { + if (marker.tags && marker.tags.indexOf(MarkerTag.Unnecessary) >= 0) { + className = ClassName.EditorUnnecessaryDecoration; + } else { className = ClassName.EditorHintDecoration; } zIndex = 0; @@ -157,9 +163,9 @@ class ModelMarkerHandler { break; } - if (marker.customTags) { - if (marker.customTags.indexOf(MarkerTag.Unnecessary) !== -1) { - inlineClassName = ClassName.EditorUnnecessaryDecoration; + if (marker.tags) { + if (marker.tags.indexOf(MarkerTag.Unnecessary) !== -1) { + inlineClassName = ClassName.EditorUnnecessaryInlineDecoration; } } @@ -272,6 +278,9 @@ export class ModelServiceImpl implements IModelService { if (!isNaN(parsedTabSize)) { tabSize = parsedTabSize; } + if (tabSize < 1) { + tabSize = 1; + } } let insertSpaces = EDITOR_MODEL_DEFAULTS.insertSpaces; @@ -492,7 +501,7 @@ export class ModelServiceImpl implements IModelService { public createModel(value: string | ITextBufferFactory, modeOrPromise: TPromise | IMode, resource: URI, isForSimpleWidget: boolean = false): ITextModel { let modelData: ModelData; - if (!modeOrPromise || TPromise.is(modeOrPromise)) { + if (!modeOrPromise || isThenable(modeOrPromise)) { modelData = this._createModelData(value, PLAINTEXT_LANGUAGE_IDENTIFIER, resource, isForSimpleWidget); this.setMode(modelData.model, modeOrPromise); } else { @@ -513,7 +522,7 @@ export class ModelServiceImpl implements IModelService { if (!modeOrPromise) { return; } - if (TPromise.is(modeOrPromise)) { + if (isThenable(modeOrPromise)) { modeOrPromise.then((mode) => { if (!model.isDisposed()) { model.setMode(mode.getLanguageIdentifier()); diff --git a/src/vs/editor/common/services/resolverService.ts b/src/vs/editor/common/services/resolverService.ts index 173713e7018..d838f202e5e 100644 --- a/src/vs/editor/common/services/resolverService.ts +++ b/src/vs/editor/common/services/resolverService.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorModel } from 'vs/platform/editor/common/editor'; @@ -33,7 +33,7 @@ export interface ITextModelContentProvider { /** * Given a resource, return the content of the resource as `ITextModel`. */ - provideTextContent(resource: URI): TPromise; + provideTextContent(resource: URI): Thenable; } export interface ITextEditorModel extends IEditorModel { @@ -42,4 +42,6 @@ export interface ITextEditorModel extends IEditorModel { * Provides access to the underlying `ITextModel`. */ textEditorModel: ITextModel; + + isReadonly(): boolean; } diff --git a/src/vs/editor/common/services/resourceConfiguration.ts b/src/vs/editor/common/services/resourceConfiguration.ts index 82ed5d415e5..8d4a738eb9b 100644 --- a/src/vs/editor/common/services/resourceConfiguration.ts +++ b/src/vs/editor/common/services/resourceConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IPosition } from 'vs/editor/common/core/position'; import { IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/editor/common/services/resourceConfigurationImpl.ts b/src/vs/editor/common/services/resourceConfigurationImpl.ts index 6b75152e474..1cdb3372d6d 100644 --- a/src/vs/editor/common/services/resourceConfigurationImpl.ts +++ b/src/vs/editor/common/services/resourceConfigurationImpl.ts @@ -5,7 +5,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IPosition, Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/common/services/webWorker.ts b/src/vs/editor/common/services/webWorker.ts index 2a8d04558d7..a07a751b041 100644 --- a/src/vs/editor/common/services/webWorker.ts +++ b/src/vs/editor/common/services/webWorker.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ShallowCancelThenPromise } from 'vs/base/common/async'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditorWorkerClient } from 'vs/editor/common/services/editorWorkerServiceImpl'; @@ -68,7 +67,7 @@ class MonacoWebWorkerImpl extends EditorWorkerClient implements MonacoWebWork private _getForeignProxy(): TPromise { if (!this._foreignProxy) { - this._foreignProxy = new ShallowCancelThenPromise(this._getProxy().then((proxy) => { + this._foreignProxy = this._getProxy().then((proxy) => { return proxy.loadForeignModule(this._foreignModuleId, this._foreignModuleCreateData).then((foreignMethods) => { this._foreignModuleId = null; this._foreignModuleCreateData = null; @@ -91,7 +90,7 @@ class MonacoWebWorkerImpl extends EditorWorkerClient implements MonacoWebWork return foreignProxy; }); - })); + }); } return this._foreignProxy; } diff --git a/src/vs/editor/common/standalone/standaloneBase.ts b/src/vs/editor/common/standalone/standaloneBase.ts index c4683e6d6ed..9bdd7c5f577 100644 --- a/src/vs/editor/common/standalone/standaloneBase.ts +++ b/src/vs/editor/common/standalone/standaloneBase.ts @@ -12,18 +12,12 @@ import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { TPromise } from 'vs/base/common/winjs.base'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Token } from 'vs/editor/common/core/token'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; // -------------------------------------------- // This is repeated here so it can be exported // because TS inlines const enums // -------------------------------------------- -export enum Severity { - Ignore = 0, - Info = 1, - Warning = 2, - Error = 3, -} export enum MarkerTag { Unnecessary = 1, @@ -248,7 +242,6 @@ export function createMonacoBaseAPI(): typeof monaco { Range: Range, Selection: Selection, SelectionDirection: SelectionDirection, - Severity: Severity, MarkerSeverity: MarkerSeverity, MarkerTag: MarkerTag, Promise: TPromise, diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index c8b310fb7de..c4c369cb0d2 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -21,9 +21,9 @@ export const editorCursorBackground = registerColor('editorCursor.background', n export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hc: '#e3e4e229' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.')); export const editorIndentGuides = registerColor('editorIndentGuide.background', { dark: editorWhitespaces, light: editorWhitespaces, hc: editorWhitespaces }, nls.localize('editorIndentGuides', 'Color of the editor indentation guides.')); export const editorActiveIndentGuides = registerColor('editorIndentGuide.activeBackground', { dark: editorWhitespaces, light: editorWhitespaces, hc: editorWhitespaces }, nls.localize('editorActiveIndentGuide', 'Color of the active editor indentation guides.')); -export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#5A5A5A', light: '#2B91AF', hc: Color.white }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); +export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hc: Color.white }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); -const deprecatedEditorActiveLineNumber = registerColor('editorActiveLineNumber.foreground', { dark: '#AAAAAA', light: '#0B216F', hc: activeContrastBorder }, nls.localize('editorActiveLineNumber', 'Color of editor active line number'), false, nls.localize('deprecatedEditorActiveLineNumber', 'Id is deprecated. Use \'editorLineNumber.activeForeground\' instead.')); +const deprecatedEditorActiveLineNumber = registerColor('editorActiveLineNumber.foreground', { dark: '#c6c6c6', light: '#0B216F', hc: activeContrastBorder }, nls.localize('editorActiveLineNumber', 'Color of editor active line number'), false, nls.localize('deprecatedEditorActiveLineNumber', 'Id is deprecated. Use \'editorLineNumber.activeForeground\' instead.')); export const editorActiveLineNumber = registerColor('editorLineNumber.activeForeground', { dark: deprecatedEditorActiveLineNumber, light: deprecatedEditorActiveLineNumber, hc: deprecatedEditorActiveLineNumber }, nls.localize('editorActiveLineNumber', 'Color of editor active line number')); export const editorRuler = registerColor('editorRuler.foreground', { dark: '#5A5A5A', light: Color.lightgrey, hc: Color.white }, nls.localize('editorRuler', 'Color of the editor rulers.')); @@ -49,7 +49,8 @@ export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hc: null }, nls.localize('hintForeground', 'Foreground color of hint squigglies in the editor.')); export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hc: Color.fromHex('#eeeeee').transparent(0.8) }, nls.localize('hintBorder', 'Border color of hint squigglies in the editor.')); -export const editorUnnecessaryForeground = registerColor('editorUnnecessary.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hc: null }, nls.localize('unnecessaryForeground', 'Foreground color of unnecessary code in the editor.')); +export const editorUnnecessaryCodeBorder = registerColor('editorUnnecessaryCode.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('unnecessaryCodeBorder', 'Border of unnecessary code in the editor.')); +export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode.opacity', { dark: Color.fromHex('#000a'), light: Color.fromHex('#0007'), hc: null }, nls.localize('unnecessaryCodeOpacity', 'Opacity of unnecessary code in the editor.')); const rulerRangeDefault = new Color(new RGBA(0, 122, 204, 0.6)); export const overviewRulerRangeHighlight = registerColor('editorOverviewRuler.rangeHighlightForeground', { dark: rulerRangeDefault, light: rulerRangeDefault, hc: rulerRangeDefault }, nls.localize('overviewRulerRangeHighlight', 'Overview ruler marker color for range highlights. The color must not be opaque to not hide underlying decorations.'), true); diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index d28e49abcfe..5efdb7f822e 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -9,7 +9,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import * as errors from 'vs/base/common/errors'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollType } from 'vs/editor/common/editorCommon'; export const enum ViewEventType { @@ -42,6 +42,7 @@ export class ViewConfigurationChangedEvent { public readonly readOnly: boolean; public readonly accessibilitySupport: boolean; public readonly emptySelectionClipboard: boolean; + public readonly copyWithSyntaxHighlighting: boolean; public readonly layoutInfo: boolean; public readonly fontInfo: boolean; public readonly viewInfo: boolean; @@ -55,6 +56,7 @@ export class ViewConfigurationChangedEvent { this.readOnly = source.readOnly; this.accessibilitySupport = source.accessibilitySupport; this.emptySelectionClipboard = source.emptySelectionClipboard; + this.copyWithSyntaxHighlighting = source.copyWithSyntaxHighlighting; this.layoutInfo = source.layoutInfo; this.fontInfo = source.fontInfo; this.viewInfo = source.viewInfo; @@ -354,17 +356,15 @@ export class ViewEventEmitter extends Disposable { public addEventListener(listener: (events: ViewEvent[]) => void): IDisposable { this._listeners.push(listener); - return { - dispose: () => { - let listeners = this._listeners; - for (let i = 0, len = listeners.length; i < len; i++) { - if (listeners[i] === listener) { - listeners.splice(i, 1); - break; - } + return toDisposable(() => { + let listeners = this._listeners; + for (let i = 0, len = listeners.length; i < len; i++) { + if (listeners[i] === listener) { + listeners.splice(i, 1); + break; } } - }; + }); } } diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 977f9a5235b..0d91e39e3e4 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -35,6 +35,7 @@ class LinePart { export class RenderLineInput { public readonly useMonospaceOptimizations: boolean; + public readonly canUseHalfwidthRightwardsArrow: boolean; public readonly lineContent: string; public readonly continuesWithWrappedLine: boolean; public readonly isBasicASCII: boolean; @@ -51,6 +52,7 @@ export class RenderLineInput { constructor( useMonospaceOptimizations: boolean, + canUseHalfwidthRightwardsArrow: boolean, lineContent: string, continuesWithWrappedLine: boolean, isBasicASCII: boolean, @@ -66,6 +68,7 @@ export class RenderLineInput { fontLigatures: boolean ) { this.useMonospaceOptimizations = useMonospaceOptimizations; + this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow; this.lineContent = lineContent; this.continuesWithWrappedLine = continuesWithWrappedLine; this.isBasicASCII = isBasicASCII; @@ -90,6 +93,7 @@ export class RenderLineInput { public equals(other: RenderLineInput): boolean { return ( this.useMonospaceOptimizations === other.useMonospaceOptimizations + && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow && this.lineContent === other.lineContent && this.continuesWithWrappedLine === other.continuesWithWrappedLine && this.isBasicASCII === other.isBasicASCII @@ -303,6 +307,7 @@ export function renderViewLine2(input: RenderLineInput): RenderLineOutput2 { class ResolvedRenderLineInput { constructor( public readonly fontIsMonospace: boolean, + public readonly canUseHalfwidthRightwardsArrow: boolean, public readonly lineContent: string, public readonly len: number, public readonly isOverflowing: boolean, @@ -358,6 +363,7 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput return new ResolvedRenderLineInput( useMonospaceOptimizations, + input.canUseHalfwidthRightwardsArrow, lineContent, len, isOverflowing, @@ -447,6 +453,7 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW let tokenIndex = 0; let tokenType = tokens[tokenIndex].type; let tokenEndIndex = tokens[tokenIndex].endIndex; + const tokensLength = tokens.length; let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); let lastNonWhitespaceIndex: number; @@ -526,8 +533,10 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW if (charIndex === tokenEndIndex) { tokenIndex++; - tokenType = tokens[tokenIndex].type; - tokenEndIndex = tokens[tokenIndex].endIndex; + if (tokenIndex < tokensLength) { + tokenType = tokens[tokenIndex].type; + tokenEndIndex = tokens[tokenIndex].endIndex; + } } } @@ -613,6 +622,7 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP */ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): RenderLineOutput { const fontIsMonospace = input.fontIsMonospace; + const canUseHalfwidthRightwardsArrow = input.canUseHalfwidthRightwardsArrow; const containsForeignElements = input.containsForeignElements; const lineContent = input.lineContent; const len = input.len; @@ -688,7 +698,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render tabsCharDelta += insertSpacesCount - 1; charOffsetInPart += insertSpacesCount - 1; if (insertSpacesCount > 0) { - if (insertSpacesCount > 1) { + if (!canUseHalfwidthRightwardsArrow || insertSpacesCount > 1) { sb.write1(0x2192); // RIGHTWARDS ARROW } else { sb.write1(0xffeb); // HALFWIDTH RIGHTWARDS ARROW diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 17ef4199216..3f34f8b4fab 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -192,6 +192,10 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // This is pretty bad, it means we lost track of the model... throw new Error(`ViewModel is out of sync with Model!`); } + if (this.lines.length !== this.model.getLineCount()) { + // This is pretty bad, it means we lost track of the model... + this._constructLines(false); + } } private _constructLines(resetHiddenAreas: boolean): void { @@ -523,7 +527,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { const result = this.model.getActiveIndentGuide(modelPosition.lineNumber, modelMinPosition.lineNumber, modelMaxPosition.lineNumber); const viewStartPosition = this.convertModelPositionToViewPosition(result.startLineNumber, 1); - const viewEndPosition = this.convertModelPositionToViewPosition(result.endLineNumber, 1); + const viewEndPosition = this.convertModelPositionToViewPosition(result.endLineNumber, this.model.getLineMaxColumn(result.endLineNumber)); return { startLineNumber: viewStartPosition.lineNumber, endLineNumber: viewEndPosition.lineNumber, diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index b2573d26fa0..ed17ffb44dc 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -22,6 +22,7 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { TrackedRangeStickiness, IModelDeltaDecoration, OverviewRulerLane } from 'vs/editor/common/model'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hc: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); @@ -34,7 +35,8 @@ class JumpToBracketAction extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH, + weight: KeybindingWeight.EditorContrib } }); } @@ -116,7 +118,13 @@ export class BracketMatchingController extends Disposable implements editorCommo this._updateBracketsSoon.schedule(); })); - this._register(editor.onDidChangeModel((e) => { this._decorations = []; this._updateBracketsSoon.schedule(); })); + this._register(editor.onDidChangeModelContent((e) => { + this._updateBracketsSoon.schedule(); + })); + this._register(editor.onDidChangeModel((e) => { + this._decorations = []; + this._updateBracketsSoon.schedule(); + })); this._register(editor.onDidChangeModelLanguageConfiguration((e) => { this._lastBracketsData = []; this._updateBracketsSoon.schedule(); diff --git a/src/vs/editor/contrib/caretOperations/transpose.ts b/src/vs/editor/contrib/caretOperations/transpose.ts index 6523327b253..ee59ca9c28d 100644 --- a/src/vs/editor/contrib/caretOperations/transpose.ts +++ b/src/vs/editor/contrib/caretOperations/transpose.ts @@ -15,6 +15,7 @@ import { registerEditorAction, EditorAction, ServicesAccessor } from 'vs/editor/ import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel } from 'vs/editor/common/model'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; class TransposeLettersAction extends EditorAction { @@ -67,7 +68,8 @@ class TransposeLettersAction extends EditorAction { primary: 0, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_T - } + }, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 97f62935e1f..4d0ec22210d 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -16,6 +16,8 @@ import { registerEditorAction, IActionOptions, EditorAction, ICommandKeybindings import { CopyOptions } from 'vs/editor/browser/controller/textAreaInput'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; @@ -62,7 +64,8 @@ class ExecCommandCutAction extends ExecCommandAction { let kbOpts: ICommandKeybindingsOptions = { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_X, - win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_X, secondary: [KeyMod.Shift | KeyCode.Delete] } + win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_X, secondary: [KeyMod.Shift | KeyCode.Delete] }, + weight: KeybindingWeight.EditorContrib }; // Do not bind cut keybindings in the browser, // since browsers do that for us and it avoids security prompts @@ -78,6 +81,12 @@ class ExecCommandCutAction extends ExecCommandAction { menuOpts: { group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 1 + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '2_ccp', + title: nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "Cu&&t"), + order: 1 } }); } @@ -99,7 +108,8 @@ class ExecCommandCopyAction extends ExecCommandAction { let kbOpts: ICommandKeybindingsOptions = { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_C, - win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] } + win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] }, + weight: KeybindingWeight.EditorContrib }; // Do not bind copy keybindings in the browser, // since browsers do that for us and it avoids security prompts @@ -116,6 +126,12 @@ class ExecCommandCopyAction extends ExecCommandAction { menuOpts: { group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 2 + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '2_ccp', + title: nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "&&Copy"), + order: 2 } }); } @@ -137,7 +153,8 @@ class ExecCommandPasteAction extends ExecCommandAction { let kbOpts: ICommandKeybindingsOptions = { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_V, - win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] } + win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] }, + weight: KeybindingWeight.EditorContrib }; // Do not bind paste keybindings in the browser, // since browsers do that for us and it avoids security prompts @@ -154,6 +171,12 @@ class ExecCommandPasteAction extends ExecCommandAction { menuOpts: { group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '2_ccp', + title: nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste"), + order: 3 } }); } @@ -169,7 +192,8 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends ExecCommandAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: null + primary: null, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index c13ef19a29d..e301f1ee621 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -3,58 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isFalsyOrEmpty, mergeSort, flatten } from 'vs/base/common/arrays'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { illegalArgument, onUnexpectedExternalError, isPromiseCanceledError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { flatten, isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionProviderRegistry, CodeActionContext, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CodeActionFilter, CodeActionKind, CodeActionTrigger } from './codeActionTrigger'; -import { Selection } from 'vs/editor/common/core/selection'; -export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger): TPromise { +export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger, token: CancellationToken = CancellationToken.None): Promise { const codeActionContext: CodeActionContext = { only: trigger && trigger.filter && trigger.filter.kind ? trigger.filter.kind.value : undefined, trigger: trigger && trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic }; - const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => { - if (!Array.isArray(providedCodeActions)) { + const promises = CodeActionProviderRegistry.all(model) + .filter(provider => { + // Avoid calling providers that we know will not return code actions of interest + return !provider.providedCodeActionKinds || provider.providedCodeActionKinds.some(providedKind => isValidActionKind(trigger && trigger.filter, providedKind)); + }) + .map(support => { + return Promise.resolve(support.provideCodeActions(model, rangeOrSelection, codeActionContext, token)).then(providedCodeActions => { + if (!Array.isArray(providedCodeActions)) { + return []; + } + return providedCodeActions.filter(action => isValidAction(trigger && trigger.filter, action)); + }, (err): CodeAction[] => { + if (isPromiseCanceledError(err)) { + throw err; + } + + onUnexpectedExternalError(err); return []; - } - return providedCodeActions.filter(action => isValidAction(trigger && trigger.filter, action)); - }, (err): CodeAction[] => { - if (isPromiseCanceledError(err)) { - throw err; - } - - onUnexpectedExternalError(err); - return []; + }); }); - }); - return TPromise.join(promises) + return Promise.all(promises) .then(flatten) .then(allCodeActions => mergeSort(allCodeActions, codeActionsComparator)); } function isValidAction(filter: CodeActionFilter | undefined, action: CodeAction): boolean { - if (!action) { - return false; - } + return action && isValidActionKind(filter, action.kind); +} +function isValidActionKind(filter: CodeActionFilter | undefined, kind: string | undefined): boolean { // Filter out actions by kind - if (filter && filter.kind && (!action.kind || !filter.kind.contains(action.kind))) { + if (filter && filter.kind && (!kind || !filter.kind.contains(kind))) { return false; } // Don't return source actions unless they are explicitly requested - if (action.kind && CodeActionKind.Source.contains(action.kind) && (!filter || !filter.includeSourceActions)) { + if (kind && CodeActionKind.Source.contains(kind) && (!filter || !filter.includeSourceActions)) { return false; } @@ -88,5 +92,5 @@ registerLanguageCommand('_executeCodeActionProvider', function (accessor, args) throw illegalArgument(); } - return getCodeActions(model, model.validateRange(range), undefined); + return getCodeActions(model, model.validateRange(range), { type: 'manual', filter: { includeSourceActions: true } }); }); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 9ea29d8e5e4..d8d998a1fb8 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancelablePromise } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeAction } from 'vs/editor/common/modes'; @@ -18,13 +21,13 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IProgressService } from 'vs/platform/progress/common/progress'; import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger'; import { CodeActionContextMenu } from './codeActionWidget'; import { LightBulbWidget } from './lightBulbWidget'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { IProgressService } from 'vs/platform/progress/common/progress'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { onUnexpectedError } from 'vs/base/common/errors'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -46,7 +49,7 @@ export class QuickFixController implements IEditorContribution { private _lightBulbWidget: LightBulbWidget; private _disposables: IDisposable[] = []; - private _activeRequest: TPromise | undefined; + private _activeRequest: CancelablePromise | undefined; constructor(editor: ICodeEditor, @IMarkerService markerService: IMarkerService, @@ -87,7 +90,7 @@ export class QuickFixController implements IEditorContribution { this._activeRequest = e.actions; } - if (e && e.trigger.filter && e.trigger.filter.kind) { + if (e && e.actions && e.trigger.filter && e.trigger.filter.kind) { // Triggered for specific scope // Apply if we only have one action or requested autoApply, otherwise show menu e.actions.then(fixes => { @@ -96,7 +99,7 @@ export class QuickFixController implements IEditorContribution { } else { this._codeActionContextMenu.show(e.actions, e.position); } - }); + }).catch(onUnexpectedError); return; } @@ -121,10 +124,12 @@ export class QuickFixController implements IEditorContribution { } private _handleLightBulbSelect(coords: { x: number, y: number }): void { - this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); + if (this._lightBulbWidget.model && this._lightBulbWidget.model.actions) { + this._codeActionContextMenu.show(this._lightBulbWidget.model.actions, coords); + } } - public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): TPromise { + public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): Thenable { return this._model.trigger({ type: 'manual', filter, autoApply }); } @@ -139,8 +144,8 @@ export class QuickFixController implements IEditorContribution { this._lightBulbWidget.title = title; } - private async _onApplyCodeAction(action: CodeAction): TPromise { - await applyCodeAction(action, this._bulkEditService, this._commandService, this._editor); + private _onApplyCodeAction(action: CodeAction): TPromise { + return TPromise.wrap(applyCodeAction(action, this._bulkEditService, this._commandService, this._editor)); } } @@ -149,7 +154,7 @@ export async function applyCodeAction( bulkEditService: IBulkEditService, commandService: ICommandService, editor?: ICodeEditor, -) { +): Promise { if (action.edit) { await bulkEditService.apply(action.edit, { editor }); } @@ -189,12 +194,13 @@ export class QuickFixAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_DOT + primary: KeyMod.CtrlCmd | KeyCode.US_DOT, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available")); } } @@ -247,7 +253,7 @@ export class CodeActionCommand extends EditorCommand { }); } - public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { + public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { const args = CodeActionCommandArgs.fromUser(userArg); return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), { kind: args.kind, includeSourceActions: true }, args.apply); } @@ -269,7 +275,8 @@ export class RefactorAction extends EditorAction { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R - } + }, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: '1_modification', @@ -281,7 +288,7 @@ export class RefactorAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), { kind: CodeActionKind.Refactor }, @@ -310,7 +317,7 @@ export class SourceAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.source.noneMessage', "No source actions available"), { kind: CodeActionKind.Source, includeSourceActions: true }, @@ -332,12 +339,13 @@ export class OrganizeImportsAction extends EditorAction { contextKeyForSupportedActions(CodeActionKind.SourceOrganizeImports)), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_O + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_O, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.organize.noneMessage', "No organize imports action available"), { kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, diff --git a/src/vs/editor/contrib/codeAction/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/codeActionContributions.ts index 8653b935470..4f7315db0f8 100644 --- a/src/vs/editor/contrib/codeAction/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/codeActionContributions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { SourceAction, QuickFixController, QuickFixAction, CodeActionCommand, RefactorAction, OrganizeImportsAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; registerEditorContribution(QuickFixController); diff --git a/src/vs/editor/contrib/codeAction/codeActionModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts index acbd68b1fa1..cba2d2ccddf 100644 --- a/src/vs/editor/contrib/codeAction/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, debounceEvent } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -14,31 +15,33 @@ import { Selection } from 'vs/editor/common/core/selection'; import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IProgressService } from 'vs/platform/progress/common/progress'; import { getCodeActions } from './codeAction'; import { CodeActionTrigger } from './codeActionTrigger'; -import { IProgressService } from 'vs/platform/progress/common/progress'; export const SUPPORTED_CODE_ACTIONS = new RawContextKey('supportedCodeAction', ''); export class CodeActionOracle { private _disposables: IDisposable[] = []; + private readonly _autoTriggerTimer = new TimeoutTimer(); constructor( private _editor: ICodeEditor, private readonly _markerService: IMarkerService, private _signalChange: (e: CodeActionsComputeEvent) => any, - delay: number = 250, + private readonly _delay: number = 250, private readonly _progressService?: IProgressService, ) { this._disposables.push( - debounceEvent(this._markerService.onMarkerChanged, (last, cur) => last ? last.concat(cur) : cur, delay / 2)(e => this._onMarkerChanges(e)), - debounceEvent(this._editor.onDidChangeCursorPosition, (last, cur) => cur, delay)(e => this._onCursorChange()) + this._markerService.onMarkerChanged(e => this._onMarkerChanges(e)), + this._editor.onDidChangeCursorPosition(() => this._onCursorChange()), ); } dispose(): void { this._disposables = dispose(this._disposables); + this._autoTriggerTimer.cancel(); } trigger(trigger: CodeActionTrigger) { @@ -48,16 +51,17 @@ export class CodeActionOracle { private _onMarkerChanges(resources: URI[]): void { const { uri } = this._editor.getModel(); - for (const resource of resources) { - if (resource.toString() === uri.toString()) { + if (resources.some(resource => resource.toString() === uri.toString())) { + this._autoTriggerTimer.cancelAndSet(() => { this.trigger({ type: 'auto' }); - return; - } + }, this._delay); } } private _onCursorChange(): void { - this.trigger({ type: 'auto' }); + this._autoTriggerTimer.cancelAndSet(() => { + this.trigger({ type: 'auto' }); + }, this._delay); } private _getRangeOfMarker(selection: Selection): Range { @@ -99,7 +103,7 @@ export class CodeActionOracle { return selection; } - private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): TPromise { + private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): Thenable { if (!selection) { // cancel this._signalChange({ @@ -113,10 +117,10 @@ export class CodeActionOracle { const model = this._editor.getModel(); const markerRange = this._getRangeOfMarker(selection); const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition(); - const actions = getCodeActions(model, selection, trigger); + const actions = createCancelablePromise(token => getCodeActions(model, selection, trigger, token)); if (this._progressService && trigger.type === 'manual') { - this._progressService.showWhile(actions, 250); + this._progressService.showWhile(TPromise.wrap(actions), 250); } this._signalChange({ @@ -134,7 +138,7 @@ export interface CodeActionsComputeEvent { trigger: CodeActionTrigger; rangeOrSelection: Range | Selection; position: Position; - actions: TPromise; + actions: CancelablePromise; } export class CodeActionModel { @@ -196,7 +200,7 @@ export class CodeActionModel { } } - trigger(trigger: CodeActionTrigger): TPromise { + trigger(trigger: CodeActionTrigger): Thenable { if (this._codeActionOracle) { return this._codeActionOracle.trigger(trigger); } diff --git a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts index 4f3c53b5dc9..b8ed8cde8fc 100644 --- a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts +++ b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts @@ -9,6 +9,7 @@ export class CodeActionKind { private static readonly sep = '.'; public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = new CodeActionKind('quickfix'); public static readonly Refactor = new CodeActionKind('refactor'); public static readonly Source = new CodeActionKind('source'); public static readonly SourceOrganizeImports = new CodeActionKind('source.organizeImports'); @@ -22,7 +23,7 @@ export class CodeActionKind { } } -export enum CodeActionAutoApply { +export const enum CodeActionAutoApply { IfSingle = 1, First = 2, Never = 3 diff --git a/src/vs/editor/contrib/codeAction/codeActionWidget.ts b/src/vs/editor/contrib/codeAction/codeActionWidget.ts index 0020eca69a4..ebf9f659b0f 100644 --- a/src/vs/editor/contrib/codeAction/codeActionWidget.ts +++ b/src/vs/editor/contrib/codeAction/codeActionWidget.ts @@ -28,9 +28,9 @@ export class CodeActionContextMenu { private readonly _onApplyCodeAction: (action: CodeAction) => TPromise ) { } - show(fixes: TPromise, at: { x: number; y: number } | Position) { + show(fixes: Thenable, at: { x: number; y: number } | Position) { - const actions = fixes.then(value => { + const actions = fixes ? fixes.then(value => { return value.map(action => { return new Action(action.command ? action.command.id : action.title, action.title, undefined, true, () => { return always( @@ -41,10 +41,10 @@ export class CodeActionContextMenu { }).then(actions => { if (!this._editor.getDomNode()) { // cancel when editor went off-dom - return TPromise.wrapError(canceled()); + return TPromise.wrapError(canceled()); } return actions; - }); + }) : TPromise.as([] as Action[]); this._contextMenuService.showContextMenu({ getAnchor: () => { @@ -53,7 +53,7 @@ export class CodeActionContextMenu { } return at; }, - getActions: () => actions, + getActions: () => TPromise.wrap(actions), onHide: () => { this._visible = false; this._editor.focus(); diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts index 61e18dd6bde..fe0c8eb7320 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -7,10 +7,11 @@ import * as dom from 'vs/base/browser/dom'; import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./lightBulbWidget'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; import { CodeActionsComputeEvent } from './codeActionModel'; export class LightBulbWidget implements IDisposable, IContentWidget { @@ -52,7 +53,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { const { lineHeight } = this._editor.getConfiguration(); let pad = Math.floor(lineHeight / 3); - if (this._position.position.lineNumber < this._model.position.lineNumber) { + if (this._position && this._position.position.lineNumber < this._model.position.lineNumber) { pad += lineHeight; } @@ -114,13 +115,18 @@ export class LightBulbWidget implements IDisposable, IContentWidget { const { token } = this._futureFixes; this._model = value; - this._model.actions.done(fixes => { + const selection = this._model.rangeOrSelection; + this._model.actions.then(fixes => { if (!token.isCancellationRequested && fixes && fixes.length > 0) { - this._show(); + if (selection.isEmpty() && fixes.every(fix => fix.kind && CodeActionKind.Refactor.contains(fix.kind))) { + this.hide(); + } else { + this._show(); + } } else { this.hide(); } - }, err => { + }).catch(err => { this.hide(); }); } @@ -142,7 +148,7 @@ export class LightBulbWidget implements IDisposable, IContentWidget { if (!config.contribInfo.lightbulbEnabled) { return; } - const { lineNumber } = this._model.position; + const { lineNumber, column } = this._model.position; const model = this._editor.getModel(); if (!model) { return; @@ -152,13 +158,21 @@ export class LightBulbWidget implements IDisposable, IContentWidget { const lineContent = model.getLineContent(lineNumber); const indent = TextModel.computeIndentLevel(lineContent, tabSize); const lineHasSpace = config.fontInfo.spaceWidth * indent > 22; + const isFolded = (lineNumber) => { + return lineNumber > 2 && this._editor.getTopForLineNumber(lineNumber) === this._editor.getTopForLineNumber(lineNumber - 1); + }; let effectiveLineNumber = lineNumber; if (!lineHasSpace) { - if (lineNumber > 1) { + if (lineNumber > 1 && !isFolded(lineNumber - 1)) { effectiveLineNumber -= 1; - } else { + } else if (!isFolded(lineNumber + 1)) { effectiveLineNumber += 1; + } else if (column * config.fontInfo.spaceWidth < 22) { + // cannot show lightbulb above/below and showing + // it inline would overlay the cursor... + this.hide(); + return; } } diff --git a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts index c2cc03ff9da..024c360fcc0 100644 --- a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -5,14 +5,14 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderRegistry, Command, LanguageIdentifier, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; -import { MarkerSeverity, IMarkerData } from 'vs/platform/markers/common/markers'; +import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; suite('CodeAction', () => { @@ -194,4 +194,27 @@ suite('CodeAction', () => { assert.strictEqual(actions[0].title, 'a'); } }); + + test('getCodeActions should not invoke code action providers filtered out by providedCodeActionKinds', async function () { + let wasInvoked = false; + const provider = new class implements CodeActionProvider { + provideCodeActions() { + wasInvoked = true; + return []; + } + + providedCodeActionKinds = [CodeActionKind.Refactor.value]; + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { + type: 'auto', + filter: { + kind: CodeActionKind.QuickFix + } + }); + assert.strictEqual(actions.length, 0); + assert.strictEqual(wasInvoked, false); + }); }); diff --git a/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts index 97ec8e79436..9635415bafb 100644 --- a/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts @@ -2,19 +2,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { MarkerService } from 'vs/platform/markers/common/markerService'; -import { CodeActionOracle } from 'vs/editor/contrib/codeAction/codeActionModel'; -import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { CodeActionProviderRegistry, LanguageIdentifier } from 'vs/editor/common/modes'; +import { CodeActionOracle } from 'vs/editor/contrib/codeAction/codeActionModel'; +import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { MarkerService } from 'vs/platform/markers/common/markerService'; +const testProvider = { + provideCodeActions() { + return [{ id: 'test-command', title: 'test', arguments: [] }]; + } +}; suite('CodeAction', () => { const languageIdentifier = new LanguageIdentifier('foo-lang', 3); @@ -22,14 +26,10 @@ suite('CodeAction', () => { let model: TextModel; let markerService: MarkerService; let editor: ICodeEditor; - let reg: IDisposable; + let disposables: IDisposable[]; setup(() => { - reg = CodeActionProviderRegistry.register(languageIdentifier.language, { - provideCodeActions() { - return [{ id: 'test-command', title: 'test', arguments: [] }]; - } - }); + disposables = []; markerService = new MarkerService(); model = TextModel.createFromString('foobar foo bar\nfarboo far boo', undefined, languageIdentifier, uri); editor = createTestCodeEditor({ model: model }); @@ -37,13 +37,15 @@ suite('CodeAction', () => { }); teardown(() => { - reg.dispose(); + dispose(disposables); editor.dispose(); model.dispose(); markerService.dispose(); }); test('Orcale -> marker added', done => { + const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider); + disposables.push(reg); const oracle = new CodeActionOracle(editor, markerService, e => { assert.equal(e.trigger.type, 'auto'); @@ -68,6 +70,8 @@ suite('CodeAction', () => { }); test('Orcale -> position changed', () => { + const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider); + disposables.push(reg); markerService.changeOne('fake', uri, [{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6, @@ -96,11 +100,12 @@ suite('CodeAction', () => { }); test('Lightbulb is in the wrong place, #29933', async function () { - let reg = CodeActionProviderRegistry.register(languageIdentifier.language, { - provideCodeActions(doc, _range) { + const reg = CodeActionProviderRegistry.register(languageIdentifier.language, { + provideCodeActions(_doc, _range) { return []; } }); + disposables.push(reg); editor.getModel().setValue('// @ts-check\n2\ncon\n'); @@ -146,9 +151,33 @@ suite('CodeAction', () => { // oracle.trigger('manual'); // }); - - - reg.dispose(); }); + test('Orcale -> should only auto trigger once for cursor and marker update right after each other', done => { + const reg = CodeActionProviderRegistry.register(languageIdentifier.language, testProvider); + disposables.push(reg); + + let triggerCount = 0; + const oracle = new CodeActionOracle(editor, markerService, e => { + assert.equal(e.trigger.type, 'auto'); + ++triggerCount; + + // give time for second trigger before completing test + setTimeout(() => { + oracle.dispose(); + assert.strictEqual(triggerCount, 1); + done(); + }, 50); + }, 5 /*delay*/); + + markerService.changeOne('fake', uri, [{ + startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6, + message: 'error', + severity: 1, + code: '', + source: '' + }]); + + editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 }); + }); }); diff --git a/src/vs/editor/contrib/codelens/codelens.ts b/src/vs/editor/contrib/codelens/codelens.ts index 92821ec5ee6..a80ad5d3680 100644 --- a/src/vs/editor/contrib/codelens/codelens.ts +++ b/src/vs/editor/contrib/codelens/codelens.ts @@ -7,13 +7,11 @@ import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { mergeSort } from 'vs/base/common/arrays'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { asWinJsPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; export interface ICodeLensData { @@ -21,20 +19,20 @@ export interface ICodeLensData { provider: CodeLensProvider; } -export function getCodeLensData(model: ITextModel): TPromise { +export function getCodeLensData(model: ITextModel, token: CancellationToken): Promise { const symbols: ICodeLensData[] = []; const provider = CodeLensProviderRegistry.ordered(model); - const promises = provider.map(provider => asWinJsPromise(token => provider.provideCodeLenses(model, token)).then(result => { + const promises = provider.map(provider => Promise.resolve(provider.provideCodeLenses(model, token)).then(result => { if (Array.isArray(result)) { for (let symbol of result) { symbols.push({ symbol, provider }); } } - }, onUnexpectedExternalError)); + }).catch(onUnexpectedExternalError)); - return TPromise.join(promises).then(() => { + return Promise.all(promises).then(() => { return mergeSort(symbols, (a, b) => { // sort by lineNumber, provider-rank, and column @@ -70,7 +68,7 @@ registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) { } const result: ICodeLensSymbol[] = []; - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { let resolve: Thenable[] = []; diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 0cd0fb98162..23fde7dc577 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -5,21 +5,20 @@ 'use strict'; -import { RunOnceScheduler, asWinJsPromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ICodeLensData, getCodeLensData } from './codelens'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; -import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; +import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; +import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes'; +import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; +import { getCodeLensData, ICodeLensData } from './codelens'; export class CodeLensContribution implements editorCommon.IEditorContribution { @@ -30,9 +29,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { private _globalToDispose: IDisposable[]; private _localToDispose: IDisposable[]; private _lenses: CodeLens[]; - private _currentFindCodeLensSymbolsPromise: TPromise; + private _currentFindCodeLensSymbolsPromise: CancelablePromise; private _modelChangeCounter: number; - private _currentFindOccPromise: TPromise; + private _currentResolveCodeLensSymbolsPromise: CancelablePromise; private _detectVisibleLenses: RunOnceScheduler; constructor( @@ -72,9 +71,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._currentFindCodeLensSymbolsPromise = null; this._modelChangeCounter++; } - if (this._currentFindOccPromise) { - this._currentFindOccPromise.cancel(); - this._currentFindOccPromise = null; + if (this._currentResolveCodeLensSymbolsPromise) { + this._currentResolveCodeLensSymbolsPromise.cancel(); + this._currentResolveCodeLensSymbolsPromise = null; } this._localToDispose = dispose(this._localToDispose); } @@ -117,7 +116,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._currentFindCodeLensSymbolsPromise.cancel(); } - this._currentFindCodeLensSymbolsPromise = getCodeLensData(model); + this._currentFindCodeLensSymbolsPromise = createCancelablePromise(token => getCodeLensData(model, token)); this._currentFindCodeLensSymbolsPromise.then((result) => { if (counterValue === this._modelChangeCounter) { // only the last one wins @@ -168,22 +167,20 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._localToDispose.push(this._editor.onDidLayoutChange(e => { this._detectVisibleLenses.schedule(); })); - this._localToDispose.push({ - dispose: () => { - if (this._editor.getModel()) { - const scrollState = StableEditorScrollState.capture(this._editor); - this._editor.changeDecorations((changeAccessor) => { - this._editor.changeViewZones((accessor) => { - this._disposeAllLenses(changeAccessor, accessor); - }); + this._localToDispose.push(toDisposable(() => { + if (this._editor.getModel()) { + const scrollState = StableEditorScrollState.capture(this._editor); + this._editor.changeDecorations((changeAccessor) => { + this._editor.changeViewZones((accessor) => { + this._disposeAllLenses(changeAccessor, accessor); }); - scrollState.restore(this._editor); - } else { - // No accessors available - this._disposeAllLenses(null, null); - } + }); + scrollState.restore(this._editor); + } else { + // No accessors available + this._disposeAllLenses(null, null); } - }); + })); scheduler.schedule(); } @@ -267,9 +264,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } private _onViewportChanged(): void { - if (this._currentFindOccPromise) { - this._currentFindOccPromise.cancel(); - this._currentFindOccPromise = null; + if (this._currentResolveCodeLensSymbolsPromise) { + this._currentResolveCodeLensSymbolsPromise.cancel(); + this._currentResolveCodeLensSymbolsPromise = null; } const model = this._editor.getModel(); @@ -291,28 +288,34 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { return; } - const promises = toResolve.map((request, i) => { + this._currentResolveCodeLensSymbolsPromise = createCancelablePromise(token => { - const resolvedSymbols = new Array(request.length); - const promises = request.map((request, i) => { - if (typeof request.provider.resolveCodeLens === 'function') { - return asWinJsPromise((token) => { - return request.provider.resolveCodeLens(model, request.symbol, token); - }).then(symbol => { - resolvedSymbols[i] = symbol; - }); - } - resolvedSymbols[i] = request.symbol; - return TPromise.as(void 0); + const promises = toResolve.map((request, i) => { + + const resolvedSymbols = new Array(request.length); + const promises = request.map((request, i) => { + if (typeof request.provider.resolveCodeLens === 'function') { + return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => { + resolvedSymbols[i] = symbol; + }); + } + resolvedSymbols[i] = request.symbol; + return Promise.resolve(void 0); + }); + + return Promise.all(promises).then(() => { + lenses[i].updateCommands(resolvedSymbols); + }); }); - return TPromise.join(promises).then(() => { - lenses[i].updateCommands(resolvedSymbols); - }); + return Promise.all(promises); }); - this._currentFindOccPromise = TPromise.join(promises).then(() => { - this._currentFindOccPromise = null; + this._currentResolveCodeLensSymbolsPromise.then(() => { + this._currentResolveCodeLensSymbolsPromise = null; + }).catch(err => { + this._currentResolveCodeLensSymbolsPromise = null; + onUnexpectedError(err); }); } } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 72475e905c0..1d5b1e08cc8 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -93,7 +93,7 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { let command = this._commands[element.id]; if (command) { editor.focus(); - commandService.executeCommand(command.id, ...command.arguments).done(undefined, err => { + commandService.executeCommand(command.id, ...command.arguments).then(undefined, err => { notificationService.error(err); }); } diff --git a/src/vs/editor/contrib/colorPicker/color.ts b/src/vs/editor/contrib/colorPicker/color.ts index e3f8d687510..b9d0c40de8e 100644 --- a/src/vs/editor/contrib/colorPicker/color.ts +++ b/src/vs/editor/contrib/colorPicker/color.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ColorProviderRegistry, DocumentColorProvider, IColorInformation, IColorPresentation } from 'vs/editor/common/modes'; -import { asWinJsPromise } from 'vs/base/common/async'; import { ITextModel } from 'vs/editor/common/model'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range, IRange } from 'vs/editor/common/core/range'; import { illegalArgument } from 'vs/base/common/errors'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IColorData { @@ -20,10 +20,10 @@ export interface IColorData { provider: DocumentColorProvider; } -export function getColors(model: ITextModel): TPromise { +export function getColors(model: ITextModel, token: CancellationToken): Promise { const colors: IColorData[] = []; const providers = ColorProviderRegistry.ordered(model).reverse(); - const promises = providers.map(provider => asWinJsPromise(token => provider.provideDocumentColors(model, token)).then(result => { + const promises = providers.map(provider => Promise.resolve(provider.provideDocumentColors(model, token)).then(result => { if (Array.isArray(result)) { for (let colorInfo of result) { colors.push({ colorInfo, provider }); @@ -31,11 +31,11 @@ export function getColors(model: ITextModel): TPromise { } })); - return TPromise.join(promises).then(() => colors); + return Promise.all(promises).then(() => colors); } -export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider): TPromise { - return asWinJsPromise(token => provider.provideColorPresentations(model, colorInfo, token)); +export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider, token: CancellationToken): Promise { + return Promise.resolve(provider.provideColorPresentations(model, colorInfo, token)); } registerLanguageCommand('_executeDocumentColorProvider', function (accessor, args) { @@ -52,7 +52,7 @@ registerLanguageCommand('_executeDocumentColorProvider', function (accessor, arg const rawCIs: { range: IRange, color: [number, number, number, number] }[] = []; const providers = ColorProviderRegistry.ordered(model).reverse(); - const promises = providers.map(provider => asWinJsPromise(token => provider.provideDocumentColors(model, token)).then(result => { + const promises = providers.map(provider => Promise.resolve(provider.provideDocumentColors(model, CancellationToken.None)).then(result => { if (Array.isArray(result)) { for (let ci of result) { rawCIs.push({ range: ci.range, color: [ci.color.red, ci.color.green, ci.color.blue, ci.color.alpha] }); @@ -84,7 +84,7 @@ registerLanguageCommand('_executeColorPresentationProvider', function (accessor, const presentations: IColorPresentation[] = []; const providers = ColorProviderRegistry.ordered(model).reverse(); - const promises = providers.map(provider => asWinJsPromise(token => provider.provideColorPresentations(model, colorInfo, token)).then(result => { + const promises = providers.map(provider => Promise.resolve(provider.provideColorPresentations(model, colorInfo, CancellationToken.None)).then(result => { if (Array.isArray(result)) { presentations.push(...result); } diff --git a/src/vs/editor/contrib/colorPicker/colorDetector.ts b/src/vs/editor/contrib/colorPicker/colorDetector.ts index 97fbb7b8852..a676d0e8c36 100644 --- a/src/vs/editor/contrib/colorPicker/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/colorDetector.ts @@ -6,7 +6,6 @@ import { RGBA } from 'vs/base/common/color'; import { hash } from 'vs/base/common/hash'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -17,6 +16,8 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { getColors, IColorData } from 'vs/editor/contrib/colorPicker/color'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; const MAX_DECORATORS = 500; @@ -28,8 +29,8 @@ export class ColorDetector implements IEditorContribution { private _globalToDispose: IDisposable[] = []; private _localToDispose: IDisposable[] = []; - private _computePromise: TPromise; - private _timeoutPromise: TPromise; + private _computePromise: CancelablePromise; + private _timeoutTimer: TimeoutTimer; private _decorationsIds: string[] = []; private _colorDatas = new Map(); @@ -61,7 +62,7 @@ export class ColorDetector implements IEditorContribution { } })); - this._timeoutPromise = null; + this._timeoutTimer = null; this._computePromise = null; this._isEnabled = this.isEnabled(); this.onModelChanged(); @@ -115,29 +116,30 @@ export class ColorDetector implements IEditorContribution { } this._localToDispose.push(this._editor.onDidChangeModelContent((e) => { - if (!this._timeoutPromise) { - this._timeoutPromise = TPromise.timeout(ColorDetector.RECOMPUTE_TIME); - this._timeoutPromise.then(() => { - this._timeoutPromise = null; + if (!this._timeoutTimer) { + this._timeoutTimer = new TimeoutTimer(); + this._timeoutTimer.cancelAndSet(() => { + this._timeoutTimer = null; this.beginCompute(); - }); + }, ColorDetector.RECOMPUTE_TIME); } })); this.beginCompute(); } private beginCompute(): void { - this._computePromise = getColors(this._editor.getModel()).then(colorInfos => { + this._computePromise = createCancelablePromise(token => getColors(this._editor.getModel(), token)); + this._computePromise.then((colorInfos) => { this.updateDecorations(colorInfos); this.updateColorDecorators(colorInfos); this._computePromise = null; - }); + }, onUnexpectedError); } private stop(): void { - if (this._timeoutPromise) { - this._timeoutPromise.cancel(); - this._timeoutPromise = null; + if (this._timeoutTimer) { + this._timeoutTimer.cancel(); + this._timeoutTimer = null; } if (this._computePromise) { this._computePromise.cancel(); diff --git a/src/vs/editor/contrib/comment/comment.ts b/src/vs/editor/contrib/comment/comment.ts index b570ff1f041..264e8527e52 100644 --- a/src/vs/editor/contrib/comment/comment.ts +++ b/src/vs/editor/contrib/comment/comment.ts @@ -12,6 +12,8 @@ import { registerEditorAction, IActionOptions, EditorAction, ServicesAccessor } import { BlockCommentCommand } from './blockCommentCommand'; import { LineCommentCommand, Type } from './lineCommentCommand'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; abstract class CommentLineAction extends EditorAction { @@ -52,7 +54,14 @@ class ToggleCommentLineAction extends CommentLineAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_SLASH + primary: KeyMod.CtrlCmd | KeyCode.US_SLASH, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '5_insert', + title: nls.localize({ key: 'miToggleLineComment', comment: ['&& denotes a mnemonic'] }, "&&Toggle Line Comment"), + order: 1 } }); } @@ -67,7 +76,8 @@ class AddLineCommentAction extends CommentLineAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), + weight: KeybindingWeight.EditorContrib } }); } @@ -82,7 +92,8 @@ class RemoveLineCommentAction extends CommentLineAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U), + weight: KeybindingWeight.EditorContrib } }); } @@ -99,7 +110,14 @@ class BlockCommentAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A } + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '5_insert', + title: nls.localize({ key: 'miToggleBlockComment', comment: ['&& denotes a mnemonic'] }, "Toggle &&Block Comment"), + order: 2 } }); } diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index abdc0f2abd0..75de2e1c6d1 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -20,6 +20,7 @@ import { IEditorContribution, IScrollEvent, ScrollType } from 'vs/editor/common/ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export interface IPosition { x: number; @@ -142,7 +143,9 @@ export class ContextMenuController implements IEditorContribution { // Disable hover const oldHoverSetting = this._editor.getConfiguration().contribInfo.hover; this._editor.updateOptions({ - hover: false + hover: { + enabled: false + } }); let menuPosition = forcedPosition; @@ -225,7 +228,8 @@ class ShowContextMenu extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.Shift | KeyCode.F10 + primary: KeyMod.Shift | KeyCode.F10, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/cursorUndo/cursorUndo.ts b/src/vs/editor/contrib/cursorUndo/cursorUndo.ts index d302c030d77..cd80e4025a1 100644 --- a/src/vs/editor/contrib/cursorUndo/cursorUndo.ts +++ b/src/vs/editor/contrib/cursorUndo/cursorUndo.ts @@ -12,6 +12,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; class CursorState { readonly selections: Selection[]; @@ -113,12 +114,13 @@ export class CursorUndo extends EditorAction { constructor() { super({ id: 'cursorUndo', - label: nls.localize('cursor.undo', "Remove Selection of Last Find Match"), - alias: 'Remove Selection of Last Find Match', + label: nls.localize('cursor.undo', "Soft Undo"), + alias: 'Soft Undo', precondition: null, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_U + primary: KeyMod.CtrlCmd | KeyCode.KEY_U, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 3d028223cee..e639b6aea8f 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -54,12 +54,20 @@ export class DragAndDropController implements editorCommon.IEditorContribution { this._toUnhook.push(this._editor.onMouseDrop((e: IEditorMouseEvent) => this._onEditorMouseDrop(e))); this._toUnhook.push(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e))); this._toUnhook.push(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e))); + this._toUnhook.push(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur())); this._dndDecorationIds = []; this._mouseDown = false; this._modiferPressed = false; this._dragSelection = null; } + private onEditorBlur() { + this._removeDecoration(); + this._dragSelection = null; + this._mouseDown = false; + this._modiferPressed = false; + } + private onEditorKeyDown(e: IKeyboardEvent): void { if (!this._editor.getConfiguration().dragAndDrop) { return; @@ -140,8 +148,8 @@ export class DragAndDropController implements editorCommon.IEditorContribution { if (this._dragSelection === null) { if (mouseEvent.event.shiftKey) { let primarySelection = this._editor.getSelection(); - let { startLineNumber, startColumn } = primarySelection; - this._editor.setSelections([new Selection(startLineNumber, startColumn, newCursorPosition.lineNumber, newCursorPosition.column)]); + let { selectionStartLineNumber, selectionStartColumn } = primarySelection; + this._editor.setSelections([new Selection(selectionStartLineNumber, selectionStartColumn, newCursorPosition.lineNumber, newCursorPosition.column)]); } else { let newSelections = this._editor.getSelections().map(selection => { if (selection.containsPosition(newCursorPosition)) { diff --git a/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg new file mode 100644 index 00000000000..d9fd295d0b6 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg new file mode 100644 index 00000000000..48e8c5a3838 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/BooleanData_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg new file mode 100644 index 00000000000..e553c3633e5 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Class_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg new file mode 100644 index 00000000000..c43aad29efd --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Class_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg new file mode 100644 index 00000000000..2af5cc6faef --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg new file mode 100644 index 00000000000..a2df3032cb1 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/ColorPalette_ColorPalette_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Constant_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Constant_16x.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Constant_16x.svg rename to src/vs/editor/contrib/documentSymbols/media/Constant_16x.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Constant_16x_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/Constant_16x_inverse.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Constant_16x_inverse.svg rename to src/vs/editor/contrib/documentSymbols/media/Constant_16x_inverse.svg diff --git a/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg new file mode 100644 index 00000000000..7b36178ab46 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Document_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg new file mode 100644 index 00000000000..bced3a467ee --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Document_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/media/EnumItem_16x.svg b/src/vs/editor/contrib/documentSymbols/media/EnumItem_16x.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/EnumItem_16x.svg rename to src/vs/editor/contrib/documentSymbols/media/EnumItem_16x.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/EnumItem_inverse_16x.svg b/src/vs/editor/contrib/documentSymbols/media/EnumItem_inverse_16x.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/EnumItem_inverse_16x.svg rename to src/vs/editor/contrib/documentSymbols/media/EnumItem_inverse_16x.svg diff --git a/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg new file mode 100755 index 00000000000..e4a9551fd5a --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Enumerator_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg new file mode 100755 index 00000000000..d8e9f4f107a --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Enumerator_inverse_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Event_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/Event_16x_vscode.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Event_16x_vscode.svg rename to src/vs/editor/contrib/documentSymbols/media/Event_16x_vscode.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Event_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/Event_16x_vscode_inverse.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Event_16x_vscode_inverse.svg rename to src/vs/editor/contrib/documentSymbols/media/Event_16x_vscode_inverse.svg diff --git a/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg new file mode 100644 index 00000000000..e1b5aa5e31d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Field_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg new file mode 100644 index 00000000000..5fc48ceff0f --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Field_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg new file mode 100644 index 00000000000..ff55f31ffa3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg new file mode 100644 index 00000000000..2f3788e7730 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Indexer_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg new file mode 100644 index 00000000000..7a80c7fe260 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg new file mode 100644 index 00000000000..ef98b5133fd --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/IntelliSenseKeyword_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg new file mode 100644 index 00000000000..0c08c8d50af --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Interface_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg new file mode 100644 index 00000000000..f7c2934a55c --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Interface_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg new file mode 100644 index 00000000000..e78894b6c63 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg new file mode 100644 index 00000000000..44a44b489d1 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/LocalVariable_16x_vscode_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg new file mode 100644 index 00000000000..e1b587f9cc0 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Method_16x.svg @@ -0,0 +1 @@ +Method_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg new file mode 100644 index 00000000000..0b7dd26efd3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Method_16x_darkp.svg @@ -0,0 +1 @@ +Method_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg new file mode 100644 index 00000000000..772b9152cb5 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg new file mode 100644 index 00000000000..dc052a068ca --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Namespace_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg new file mode 100644 index 00000000000..ac848f89b8c --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg new file mode 100644 index 00000000000..4144eea0c06 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Numeric_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Operator_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/Operator_16x_vscode.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Operator_16x_vscode.svg rename to src/vs/editor/contrib/documentSymbols/media/Operator_16x_vscode.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Operator_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/Operator_16x_vscode_inverse.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Operator_16x_vscode_inverse.svg rename to src/vs/editor/contrib/documentSymbols/media/Operator_16x_vscode_inverse.svg diff --git a/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg new file mode 100644 index 00000000000..cac629e1132 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Property_16x.svg @@ -0,0 +1 @@ +Property_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg new file mode 100644 index 00000000000..bad83c9a321 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Property_16x_darkp.svg @@ -0,0 +1 @@ +Property_16x \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg new file mode 100644 index 00000000000..640c247786e --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg new file mode 100644 index 00000000000..0fb4b8bc9e3 --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/Snippet_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/String_16x.svg b/src/vs/editor/contrib/documentSymbols/media/String_16x.svg new file mode 100644 index 00000000000..880d50dd03f --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/String_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg b/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg new file mode 100644 index 00000000000..de3ea3b37eb --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/String_16x_darkp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Structure_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/Structure_16x_vscode.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Structure_16x_vscode.svg rename to src/vs/editor/contrib/documentSymbols/media/Structure_16x_vscode.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Structure_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/Structure_16x_vscode_inverse.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Structure_16x_vscode_inverse.svg rename to src/vs/editor/contrib/documentSymbols/media/Structure_16x_vscode_inverse.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Template_16x_vscode.svg b/src/vs/editor/contrib/documentSymbols/media/Template_16x_vscode.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Template_16x_vscode.svg rename to src/vs/editor/contrib/documentSymbols/media/Template_16x_vscode.svg diff --git a/src/vs/workbench/parts/outline/electron-browser/media/Template_16x_vscode_inverse.svg b/src/vs/editor/contrib/documentSymbols/media/Template_16x_vscode_inverse.svg similarity index 100% rename from src/vs/workbench/parts/outline/electron-browser/media/Template_16x_vscode_inverse.svg rename to src/vs/editor/contrib/documentSymbols/media/Template_16x_vscode_inverse.svg diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css new file mode 100644 index 00000000000..f62841df77d --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration { + /* make sure selection color wins when a label is being selected */ + color: inherit !important; +} + +.monaco-tree .outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-tree .outline-element .outline-element-icon { + padding-right: 3px; +} + +/* .monaco-tree.no-icons .outline-element .outline-element-icon { + display: none; +} */ + +.monaco-tree .outline-element .outline-element-label { + text-overflow: ellipsis; + overflow: hidden; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-label .monaco-highlighted-label .highlight { + font-weight: bold; +} + +.monaco-tree .outline-element .outline-element-detail { + visibility: hidden; + flex: 1; + flex-basis: 10%; + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + font-size: 90%; + padding-left: 4px; + padding-top: 3px; +} + +.monaco-tree .monaco-tree-row.focused .outline-element .outline-element-detail { + visibility: inherit; +} + +.monaco-tree .outline-element .outline-element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-tree .outline-element .outline-element-decoration.bubble { + font-family: octicons; + font-size: 14px; + opacity: 0.4; +} diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css new file mode 100644 index 00000000000..76d67d5efcc --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css @@ -0,0 +1,276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .symbol-icon { + display: inline-block; + height: 14px; + width: 16px; + min-height: 14px; + min-width: 16px; +} + +/* default icons */ +.monaco-workbench .symbol-icon { + background-image: url('Field_16x.svg'); + background-repeat: no-repeat; +} +.vs-dark .monaco-workbench .symbol-icon, +.hc-black .monaco-workbench .symbol-icon { + background-image: url('Field_16x_darkp.svg'); +} + +/* constant */ +.monaco-workbench .symbol-icon.constant { + background-image: url('Constant_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.constant, +.hc-black .monaco-workbench .symbol-icon.constant { + background-image: url('Constant_16x_inverse.svg'); +} + +/* enum */ +.monaco-workbench .symbol-icon.enum { + background-image: url('Enumerator_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.enum, +.hc-black .monaco-workbench .symbol-icon.enum { + background-image: url('Enumerator_inverse_16x.svg'); +} + +/* enum-member */ +.monaco-workbench .symbol-icon.enum-member { + background-image: url('EnumItem_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.enum-member, +.hc-black .monaco-workbench .symbol-icon.enum-member { + background-image: url('EnumItem_inverse_16x.svg'); +} + +/* struct */ +.monaco-workbench .symbol-icon.struct { + background-image: url('Structure_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.struct, +.hc-black .monaco-workbench .symbol-icon.struct { + background-image: url('Structure_16x_vscode_inverse.svg'); +} + +/* event */ +.monaco-workbench .symbol-icon.event { + background-image: url('Event_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.event, +.hc-black .monaco-workbench .symbol-icon.event { + background-image: url('Event_16x_vscode_inverse.svg'); +} + +/* operator */ +.monaco-workbench .symbol-icon.operator { + background-image: url('Operator_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.operator, +.hc-black .monaco-workbench .symbol-icon.operator { + background-image: url('Operator_16x_vscode_inverse.svg'); +} + +/* type paramter */ +.monaco-workbench .symbol-icon.type-parameter { + background-image: url('Template_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.type-parameter, +.hc-black .monaco-workbench .symbol-icon.type-parameter { + background-image: url('Template_16x_vscode_inverse.svg'); +} + +/* boolean, null */ +.monaco-workbench .symbol-icon.boolean { + background-image: url('BooleanData_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.boolean, +.hc-black .monaco-workbench .symbol-icon.boolean { + background-image: url('BooleanData_16x_darkp.svg'); +} + +/* null */ +.monaco-workbench .symbol-icon.null { + background-image: url('BooleanData_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.null, +.hc-black .monaco-workbench .symbol-icon.null { + background-image: url('BooleanData_16x_darkp.svg'); +} + +/* class */ +.monaco-workbench .symbol-icon.class { + background-image: url('Class_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.class, +.hc-black .monaco-workbench .symbol-icon.class { + background-image: url('Class_16x_darkp.svg'); +} + +/* constructor */ +.monaco-workbench .symbol-icon.constructor { + background-image: url('Method_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.constructor, +.hc-black .monaco-workbench .symbol-icon.constructor { + background-image: url('Method_16x_darkp.svg'); +} + +/* file */ +.monaco-workbench .symbol-icon.file { + background-image: url('Document_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.file, +.hc-black .monaco-workbench .symbol-icon.file { + background-image: url('Document_16x_darkp.svg'); +} + +/* field */ +.monaco-workbench .symbol-icon.field { + background-image: url('Field_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.field, +.hc-black .monaco-workbench .symbol-icon.field { + background-image: url('Field_16x_darkp.svg'); +} + +/* variable */ +.monaco-workbench .symbol-icon.variable { + background-image: url('LocalVariable_16x_vscode.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.variable, +.hc-black .monaco-workbench .symbol-icon.variable { + background-image: url('LocalVariable_16x_vscode_inverse.svg'); +} + +/* array */ +.monaco-workbench .symbol-icon.array { + background-image: url('Indexer_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.array, +.hc-black .monaco-workbench .symbol-icon.array { + background-image: url('Indexer_16x_darkp.svg'); +} + +/* keyword */ +/* todo@joh not used? */ +.monaco-workbench .symbol-icon.keyword { + background-image: url('IntelliSenseKeyword_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.keyword, +.hc-black .monaco-workbench .symbol-icon.keyword { + background-image: url('IntelliSenseKeyword_16x_darkp.svg'); +} + +/* interface */ +.monaco-workbench .symbol-icon.interface { + background-image: url('Interface_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.interface, +.hc-black .monaco-workbench .symbol-icon.interface { + background-image: url('Interface_16x_darkp.svg'); +} + +/* method */ +.monaco-workbench .symbol-icon.method { + background-image: url('Method_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.method, +.hc-black .monaco-workbench .symbol-icon.method { + background-image: url('Method_16x_darkp.svg'); +} + +/* function */ +.monaco-workbench .symbol-icon.function { + background-image: url('Method_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.function, +.hc-black .monaco-workbench .symbol-icon.function { + background-image: url('Method_16x_darkp.svg'); +} + +/* object */ +.monaco-workbench .symbol-icon.object { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.object, +.hc-black .monaco-workbench .symbol-icon.object { + background-image: url('Namespace_16x_darkp.svg'); +} + +/* namespace */ +.monaco-workbench .symbol-icon.namespace { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.namespace, +.hc-black .monaco-workbench .symbol-icon.namespace { + background-image: url('Namespace_16x_darkp.svg'); +} + +/* package */ +.monaco-workbench .symbol-icon.package { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.package, +.hc-black .monaco-workbench .symbol-icon.package { + background-image: url('Namespace_16x_darkp.svg'); +} + +/* module */ +.monaco-workbench .symbol-icon.module { + background-image: url('Namespace_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.module, +.hc-black .monaco-workbench .symbol-icon.module { + background-image: url('Namespace_16x_darkp.svg'); +} + +/* number */ +.monaco-workbench .symbol-icon.number { + background-image: url('Numeric_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.number, +.hc-black .monaco-workbench .symbol-icon.number { + background-image: url('Numeric_16x_darkp.svg'); +} + +/* property */ +.monaco-workbench .symbol-icon.property { + background-image: url('Property_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.property, +.hc-black .monaco-workbench .symbol-icon.property { + background-image: url('Property_16x_darkp.svg'); +} + +/* snippet */ +/* todo@joh unused? */ +.monaco-workbench .symbol-icon.snippet { + background-image: url('Snippet_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.snippet, +.hc-black .monaco-workbench .symbol-icon.snippet { + background-image: url('Snippet_16x_darkp.svg'); +} + +/* string */ +.monaco-workbench .symbol-icon.string { + background-image: url('String_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.string, +.hc-black .monaco-workbench .symbol-icon.string { + background-image: url('String_16x_darkp.svg'); +} + +/* key */ +.monaco-workbench .symbol-icon.key { + background-image: url('String_16x.svg'); +} +.vs-dark .monaco-workbench .symbol-icon.key, +.hc-black .monaco-workbench .symbol-icon.key { + background-image: url('String_16x_darkp.svg'); +} diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts new file mode 100644 index 00000000000..c980bd1f32c --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -0,0 +1,447 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { binarySearch, coalesce, isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { first, forEach, size } from 'vs/base/common/collections'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; +import { LRUCache } from 'vs/base/common/map'; +import { commonPrefixLength } from 'vs/base/common/strings'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; + +export abstract class TreeElement { + + abstract id: string; + abstract children: { [id: string]: TreeElement }; + abstract parent: TreeElement; + + abstract adopt(newParent: TreeElement): TreeElement; + + remove(): void { + delete this.parent.children[this.id]; + } + + static findId(candidate: DocumentSymbol | string, container: TreeElement): string { + // complex id-computation which contains the origin/extension, + // the parent path, and some dedupe logic when names collide + let candidateId: string; + if (typeof candidate === 'string') { + candidateId = `${container.id}/${candidate}`; + } else { + candidateId = `${container.id}/${candidate.name}`; + if (container.children[candidateId] !== void 0) { + candidateId = `${container.id}/${candidate.name}_${candidate.range.startLineNumber}_${candidate.range.startColumn}`; + } + } + + let id = candidateId; + for (let i = 0; container.children[id] !== void 0; i++) { + id = `${candidateId}_${i}`; + } + + return id; + } + + static getElementById(id: string, element: TreeElement): TreeElement { + if (!id) { + return undefined; + } + let len = commonPrefixLength(id, element.id); + if (len === id.length) { + return element; + } + if (len < element.id.length) { + return undefined; + } + for (const key in element.children) { + let candidate = TreeElement.getElementById(id, element.children[key]); + if (candidate) { + return candidate; + } + } + return undefined; + } + + static size(element: TreeElement): number { + let res = 1; + for (const key in element.children) { + res += TreeElement.size(element.children[key]); + } + return res; + } + + static empty(element: TreeElement): boolean { + for (const _key in element.children) { + return false; + } + return true; + } +} + +export class OutlineElement extends TreeElement { + + children: { [id: string]: OutlineElement; } = Object.create(null); + score: FuzzyScore = [0, []]; + marker: { count: number, topSev: MarkerSeverity }; + + constructor( + readonly id: string, + public parent: OutlineModel | OutlineGroup | OutlineElement, + readonly symbol: DocumentSymbol + ) { + super(); + } + + adopt(parent: OutlineModel | OutlineGroup | OutlineElement): OutlineElement { + let res = new OutlineElement(this.id, parent, this.symbol); + forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res)); + return res; + } +} + +export class OutlineGroup extends TreeElement { + + children: { [id: string]: OutlineElement; } = Object.create(null); + + constructor( + readonly id: string, + public parent: OutlineModel, + readonly provider: DocumentSymbolProvider, + readonly providerIndex: number, + ) { + super(); + } + + adopt(parent: OutlineModel): 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 { + 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 { + item.score = fuzzyScore(pattern, item.symbol.name, undefined, true); + if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) { + topMatch = item; + } + for (const key in item.children) { + let child = item.children[key]; + topMatch = this._updateMatches(pattern, child, topMatch); + if (!item.score && child.score) { + // don't filter parents with unfiltered children + item.score = [0, []]; + } + } + return topMatch; + } + + getItemEnclosingPosition(position: IPosition): OutlineElement { + return this._getItemEnclosingPosition(position, this.children); + } + + private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement { + for (let key in children) { + let item = children[key]; + if (!Range.containsPosition(item.symbol.range, position)) { + continue; + } + return this._getItemEnclosingPosition(position, item.children) || item; + } + return undefined; + } + + updateMarker(marker: IMarker[]): void { + for (const key in this.children) { + this._updateMarker(marker, this.children[key]); + } + } + + private _updateMarker(markers: IMarker[], item: OutlineElement): void { + + item.marker = undefined; + + // find the proper start index to check for item/marker overlap. + let idx = binarySearch(markers, item.symbol.range, Range.compareRangesUsingStarts); + let start: number; + if (idx < 0) { + start = ~idx; + if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) { + start -= 1; + } + } else { + start = idx; + } + + let myMarkers: IMarker[] = []; + let myTopSev: MarkerSeverity; + + 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; + if (!myTopSev || marker.severity > myTopSev) { + myTopSev = marker.severity; + } + } + + // Recurse into children and let them match markers that have matched + // this outline element. This might remove markers from this element and + // therefore we remember that we have had markers. That allows us to render + // the dot, saying 'this element has children with markers' + for (const key in item.children) { + this._updateMarker(myMarkers, item.children[key]); + } + + if (myTopSev) { + item.marker = { + count: myMarkers.length, + topSev: myTopSev + }; + } + + coalesce(markers, true); + } +} + +export class OutlineModel extends TreeElement { + + private static readonly _requests = new LRUCache, model: OutlineModel }>(9, .75); + private static readonly _keys = new class { + + private _counter = 1; + private _data = new WeakMap(); + + for(textModel: ITextModel): string { + return `${textModel.id}/${textModel.getVersionId()}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`; + } + + private _hash(providers: DocumentSymbolProvider[]): string { + let result = ''; + for (const provider of providers) { + let n = this._data.get(provider); + if (typeof n === 'undefined') { + n = this._counter++; + this._data.set(provider, n); + } + result += n; + } + return result; + } + }; + + + static create(textModel: ITextModel, token: CancellationToken): Promise { + + let key = this._keys.for(textModel); + let data = OutlineModel._requests.get(key); + + if (!data) { + let source = new CancellationTokenSource(); + data = { + promiseCnt: 0, + source, + promise: OutlineModel._create(textModel, source.token), + model: undefined, + }; + OutlineModel._requests.set(key, data); + } + + if (data.model) { + // resolved -> return data + return Promise.resolve(data.model); + } + + // increase usage counter + data.promiseCnt += 1; + + token.onCancellationRequested(() => { + // last -> cancel provider request, remove cached promise + if (--data.promiseCnt === 0) { + data.source.cancel(); + OutlineModel._requests.delete(key); + } + }); + + return new Promise((resolve, reject) => { + data.promise.then(model => { + data.model = model; + resolve(model); + }, err => { + OutlineModel._requests.delete(key); + reject(err); + }); + }); + } + + static _create(textModel: ITextModel, token: CancellationToken): Promise { + + let result = new OutlineModel(textModel); + let promises = DocumentSymbolProviderRegistry.ordered(textModel).map((provider, index) => { + + let id = TreeElement.findId(`provider_${index}`, result); + let group = new OutlineGroup(id, result, provider, index); + + return Promise.resolve(provider.provideDocumentSymbols(result.textModel, token)).then(result => { + if (!isFalsyOrEmpty(result)) { + for (const info of result) { + OutlineModel._makeOutlineElement(info, group); + } + } + return group; + }, err => { + onUnexpectedExternalError(err); + return group; + }).then(group => { + if (!TreeElement.empty(group)) { + result._groups[id] = group; + } else { + group.remove(); + } + }); + }); + + return Promise.all(promises).then(() => result._compact()); + } + + private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void { + let id = TreeElement.findId(info, container); + let res = new OutlineElement(id, container, info); + if (info.children) { + for (const childInfo of info.children) { + OutlineModel._makeOutlineElement(childInfo, res); + } + } + container.children[res.id] = res; + } + + static get(element: TreeElement): OutlineModel { + while (element) { + if (element instanceof OutlineModel) { + return element; + } + element = element.parent; + } + return undefined; + } + + readonly id = 'root'; + readonly parent = undefined; + + protected _groups: { [id: string]: OutlineGroup; } = Object.create(null); + children: { [id: string]: OutlineGroup | OutlineElement; } = Object.create(null); + + protected constructor(readonly textModel: ITextModel) { + super(); + } + + adopt(): OutlineModel { + let res = new OutlineModel(this.textModel); + forEach(this._groups, entry => res._groups[entry.key] = entry.value.adopt(res)); + return res._compact(); + } + + private _compact(): this { + let count = 0; + for (const key in this._groups) { + let group = this._groups[key]; + if (first(group.children) === undefined) { // empty + delete this._groups[key]; + } else { + count += 1; + } + } + if (count !== 1) { + // + this.children = this._groups; + } else { + // adopt all elements of the first group + let group = first(this._groups); + for (let key in group.children) { + let child = group.children[key]; + child.parent = this; + this.children[child.id] = child; + } + } + return this; + } + + merge(other: OutlineModel): boolean { + if (this.textModel.uri.toString() !== other.textModel.uri.toString()) { + return false; + } + if (size(this._groups) !== size(other._groups)) { + return false; + } + this._groups = other._groups; + this.children = other.children; + return true; + } + + private _matches: [string, OutlineElement]; + + updateMatches(pattern: string): OutlineElement { + if (this._matches && this._matches[0] === pattern) { + return this._matches[1]; + } + let topMatch: OutlineElement; + for (const key in this._groups) { + topMatch = this._groups[key].updateMatches(pattern, topMatch); + } + this._matches = [pattern, topMatch]; + return topMatch; + } + + getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement { + + let preferredGroup: OutlineGroup; + if (context) { + let candidate = context.parent; + while (candidate && !preferredGroup) { + if (candidate instanceof OutlineGroup) { + preferredGroup = candidate; + } + candidate = candidate.parent; + } + } + + let result: OutlineElement = undefined; + for (const key in this._groups) { + const group = this._groups[key]; + result = group.getItemEnclosingPosition(position); + if (result && (!preferredGroup || preferredGroup === group)) { + break; + } + } + return result; + } + + getItemById(id: string): TreeElement { + return TreeElement.getElementById(id, this); + } + + updateMarker(marker: IMarker[]): void { + // sort markers by start range so that we can use + // outline element starts for quicker look up + marker.sort(Range.compareRangesUsingStarts); + + for (const key in this._groups) { + this._groups[key].updateMarker(marker.slice(0)); + } + } +} diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts new file mode 100644 index 00000000000..6737e50335b --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -0,0 +1,318 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as dom from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { values } from 'vs/base/common/collections'; +import { createMatches } from 'vs/base/common/filters'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/outlineTree'; +import 'vs/css!./media/symbol-icons'; +import { Range } from 'vs/editor/common/core/range'; +import { SymbolKind, symbolKindToCssClass } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export const enum OutlineItemCompareType { + ByPosition, + ByName, + ByKind +} + +export class OutlineItemComparator implements ISorter { + + constructor( + public type: OutlineItemCompareType = OutlineItemCompareType.ByPosition + ) { } + + compare(tree: ITree, a: OutlineGroup | OutlineElement, b: OutlineGroup | OutlineElement): number { + + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.providerIndex - b.providerIndex; + } + + if (a instanceof OutlineElement && b instanceof OutlineElement) { + switch (this.type) { + case OutlineItemCompareType.ByKind: + return a.symbol.kind - b.symbol.kind; + case OutlineItemCompareType.ByName: + return a.symbol.name.localeCompare(b.symbol.name); + case OutlineItemCompareType.ByPosition: + default: + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + } + } + + return 0; + } +} + +export class OutlineItemFilter implements IFilter { + + enabled: boolean = true; + + isVisible(tree: ITree, element: OutlineElement | any): boolean { + if (!this.enabled) { + return true; + } + return !(element instanceof OutlineElement) || Boolean(element.score); + } +} + +export class OutlineDataSource implements IDataSource { + + // this is a workaround for the tree showing twisties for items + // with only filtered children + filterOnScore: boolean = true; + + getId(tree: ITree, element: TreeElement): string { + return element ? element.id : 'empty'; + } + + hasChildren(tree: ITree, element: OutlineModel | OutlineGroup | OutlineElement): boolean { + if (!element) { + return false; + } + if (element instanceof OutlineModel) { + return true; + } + if (element instanceof OutlineElement && (this.filterOnScore && !element.score)) { + return false; + } + for (const id in element.children) { + if (!this.filterOnScore || element.children[id].score) { + return true; + } + } + return false; + } + + getChildren(tree: ITree, element: TreeElement): TPromise { + let res = values(element.children); + // console.log(element.id + ' with children ' + res.length); + return TPromise.wrap(res); + } + + getParent(tree: ITree, element: TreeElement | any): TPromise { + return TPromise.wrap(element && element.parent); + } + + shouldAutoexpand(tree: ITree, element: TreeElement): boolean { + return element && (element instanceof OutlineModel || element.parent instanceof OutlineModel || element instanceof OutlineGroup || element.parent instanceof OutlineGroup); + } +} + +export interface OutlineTemplate { + labelContainer: HTMLElement; + label: HighlightedLabel; + icon?: HTMLElement; + detail?: HTMLElement; + decoration?: HTMLElement; +} + +export class OutlineRenderer implements IRenderer { + + renderProblemColors = true; + renderProblemBadges = true; + + constructor( + @IThemeService readonly _themeService: IThemeService, + @IConfigurationService readonly _configurationService: IConfigurationService + ) { + // + } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: OutlineGroup | OutlineElement): string { + return element instanceof OutlineGroup ? 'outline-group' : 'outline-element'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement): OutlineTemplate { + if (templateId === 'outline-element') { + const icon = dom.$('.outline-element-icon symbol-icon'); + const labelContainer = dom.$('.outline-element-label'); + const detail = dom.$('.outline-element-detail'); + const decoration = dom.$('.outline-element-decoration'); + dom.addClass(container, 'outline-element'); + dom.append(container, icon, labelContainer, detail, decoration); + return { icon, labelContainer, label: new HighlightedLabel(labelContainer), detail, decoration }; + } + if (templateId === 'outline-group') { + const labelContainer = dom.$('.outline-element-label'); + dom.addClass(container, 'outline-element'); + dom.append(container, labelContainer); + return { labelContainer, label: new HighlightedLabel(labelContainer) }; + } + + throw new Error(templateId); + } + + renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { + if (element instanceof OutlineElement) { + template.icon.className = `outline-element-icon ${symbolKindToCssClass(element.symbol.kind)}`; + template.label.set(element.symbol.name, element.score ? createMatches(element.score[1]) : undefined, localize('title.template', "{0} ({1})", element.symbol.name, OutlineRenderer._symbolKindNames[element.symbol.kind])); + template.detail.innerText = element.symbol.detail || ''; + this._renderMarkerInfo(element, template); + + } + if (element instanceof OutlineGroup) { + template.label.set(element.provider.displayName || localize('provider', "Outline Provider")); + } + } + + private _renderMarkerInfo(element: OutlineElement, template: OutlineTemplate): void { + + if (!element.marker) { + dom.hide(template.decoration); + template.labelContainer.style.removeProperty('--outline-element-color'); + return; + } + + const { count, topSev } = element.marker; + const color = this._themeService.getTheme().getColor(topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); + const cssColor = color ? color.toString() : 'inherit'; + + // color of the label + if (this.renderProblemColors) { + template.labelContainer.style.setProperty('--outline-element-color', cssColor); + } else { + template.labelContainer.style.removeProperty('--outline-element-color'); + } + + // badge with color/rollup + if (!this.renderProblemBadges) { + dom.hide(template.decoration); + + } else if (count > 0) { + dom.show(template.decoration); + dom.removeClass(template.decoration, 'bubble'); + template.decoration.innerText = count < 10 ? count.toString() : '+9'; + template.decoration.title = count === 1 ? localize('1.problem', "1 problem in this element") : localize('N.problem', "{0} problems in this element", count); + template.decoration.style.setProperty('--outline-element-color', cssColor); + + } else { + dom.show(template.decoration); + dom.addClass(template.decoration, 'bubble'); + template.decoration.innerText = '\uf052'; + template.decoration.title = localize('deep.problem', "Contains elements with problems"); + template.decoration.style.setProperty('--outline-element-color', cssColor); + } + } + + private static _symbolKindNames: { [symbol: number]: string } = { + [SymbolKind.Array]: localize('Array', "array"), + [SymbolKind.Boolean]: localize('Boolean', "boolean"), + [SymbolKind.Class]: localize('Class', "class"), + [SymbolKind.Constant]: localize('Constant', "constant"), + [SymbolKind.Constructor]: localize('Constructor', "constructor"), + [SymbolKind.Enum]: localize('Enum', "enumeration"), + [SymbolKind.EnumMember]: localize('EnumMember', "enumeration member"), + [SymbolKind.Event]: localize('Event', "event"), + [SymbolKind.Field]: localize('Field', "field"), + [SymbolKind.File]: localize('File', "file"), + [SymbolKind.Function]: localize('Function', "function"), + [SymbolKind.Interface]: localize('Interface', "interface"), + [SymbolKind.Key]: localize('Key', "key"), + [SymbolKind.Method]: localize('Method', "method"), + [SymbolKind.Module]: localize('Module', "module"), + [SymbolKind.Namespace]: localize('Namespace', "namespace"), + [SymbolKind.Null]: localize('Null', "null"), + [SymbolKind.Number]: localize('Number', "number"), + [SymbolKind.Object]: localize('Object', "object"), + [SymbolKind.Operator]: localize('Operator', "operator"), + [SymbolKind.Package]: localize('Package', "package"), + [SymbolKind.Property]: localize('Property', "property"), + [SymbolKind.String]: localize('String', "string"), + [SymbolKind.Struct]: localize('Struct', "struct"), + [SymbolKind.TypeParameter]: localize('TypeParameter', "type parameter"), + [SymbolKind.Variable]: localize('Variable', "variable"), + }; + + disposeTemplate(tree: ITree, templateId: string, template: OutlineTemplate): void { + template.label.dispose(); + } + +} + +export class OutlineTreeState { + + readonly selected: string; + readonly focused: string; + readonly expanded: string[]; + + static capture(tree: ITree): OutlineTreeState { + // selection + let selected: string; + let element = tree.getSelection()[0]; + if (element instanceof TreeElement) { + selected = element.id; + } + + // focus + let focused: string; + element = tree.getFocus(true); + if (element instanceof TreeElement) { + focused = element.id; + } + + // expansion + let expanded = new Array(); + let nav = tree.getNavigator(); + while (nav.next()) { + let element = nav.current(); + if (element instanceof TreeElement) { + if (tree.isExpanded(element)) { + expanded.push(element.id); + } + } + } + return { selected, focused, expanded }; + } + + static async restore(tree: ITree, state: OutlineTreeState, eventPayload: any): Promise { + let model = tree.getInput(); + if (!state || !(model instanceof OutlineModel)) { + return TPromise.as(undefined); + } + + // expansion + let items: TreeElement[] = []; + for (const id of state.expanded) { + let item = model.getItemById(id); + if (item) { + items.push(item); + } + } + await tree.collapseAll(undefined); + await tree.expandAll(items); + + // selection & focus + let selected = model.getItemById(state.selected); + let focused = model.getItemById(state.focused); + tree.setSelection([selected], eventPayload); + tree.setFocus(focused, eventPayload); + } +} + +export class OutlineController extends WorkbenchTreeController { + protected shouldToggleExpansion(element: any, event: IMouseEvent, origin: string): boolean { + if (element instanceof OutlineElement) { + return this.isClickOnTwistie(event); + } else { + return super.shouldToggleExpansion(element, event, origin); + } + } +} diff --git a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts new file mode 100644 index 00000000000..0f46a22ff7c --- /dev/null +++ b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { OutlineElement, OutlineGroup, OutlineModel } from '../outlineModel'; +import { SymbolKind, DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; +import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; + +suite('OutlineModel', function () { + + test('OutlineModel#create, cached', async function () { + + let model = TextModel.createFromString('foo', undefined, undefined, URI.file('/fome/path.foo')); + let count = 0; + let reg = DocumentSymbolProviderRegistry.register({ pattern: '**/path.foo' }, { + provideDocumentSymbols() { + count += 1; + return []; + } + }); + + await OutlineModel.create(model, CancellationToken.None); + assert.equal(count, 1); + + // cached + await OutlineModel.create(model, CancellationToken.None); + assert.equal(count, 1); + + // new version + model.applyEdits([{ text: 'XXX', range: new Range(1, 1, 1, 1) }]); + await OutlineModel.create(model, CancellationToken.None); + assert.equal(count, 2); + + reg.dispose(); + }); + + test('OutlineModel#create, cached/cancel', async function () { + + let model = TextModel.createFromString('foo', undefined, undefined, URI.file('/fome/path.foo')); + let isCancelled = false; + + let reg = DocumentSymbolProviderRegistry.register({ pattern: '**/path.foo' }, { + provideDocumentSymbols(d, token) { + return new Promise(resolve => { + token.onCancellationRequested(_ => { + isCancelled = true; + resolve(null); + }); + }); + } + }); + + assert.equal(isCancelled, false); + let s1 = new CancellationTokenSource(); + OutlineModel.create(model, s1.token); + let s2 = new CancellationTokenSource(); + OutlineModel.create(model, s2.token); + + s1.cancel(); + assert.equal(isCancelled, false); + + s2.cancel(); + assert.equal(isCancelled, true); + + reg.dispose(); + }); + + function fakeSymbolInformation(range: Range, name: string = 'foo'): DocumentSymbol { + return { + name, + detail: 'fake', + kind: SymbolKind.Boolean, + selectionRange: range, + range: range + }; + } + + function fakeMarker(range: Range): IMarker { + return { ...range, owner: 'ffff', message: 'test', severity: MarkerSeverity.Error, resource: null }; + } + + test('OutlineElement - updateMarker', function () { + + let e0 = new OutlineElement('foo1', null, fakeSymbolInformation(new Range(1, 1, 1, 10))); + let e1 = new OutlineElement('foo2', null, fakeSymbolInformation(new Range(2, 1, 5, 1))); + let e2 = new OutlineElement('foo3', null, fakeSymbolInformation(new Range(6, 1, 10, 10))); + + let group = new OutlineGroup('group', null, null, 1); + group.children[e0.id] = e0; + group.children[e1.id] = e1; + group.children[e2.id] = e2; + + const data = [fakeMarker(new Range(6, 1, 6, 7)), fakeMarker(new Range(1, 1, 1, 4)), fakeMarker(new Range(10, 2, 14, 1))]; + data.sort(Range.compareRangesUsingStarts); // model does this + + group.updateMarker(data); + assert.equal(data.length, 0); // all 'stolen' + assert.equal(e0.marker.count, 1); + assert.equal(e1.marker, undefined); + assert.equal(e2.marker.count, 2); + + group.updateMarker([]); + assert.equal(e0.marker, undefined); + assert.equal(e1.marker, undefined); + assert.equal(e2.marker, undefined); + }); + + test('OutlineElement - updateMarker, 2', function () { + + let p = new OutlineElement('A', null, fakeSymbolInformation(new Range(1, 1, 11, 1))); + let c1 = new OutlineElement('A/B', null, fakeSymbolInformation(new Range(2, 4, 5, 4))); + let c2 = new OutlineElement('A/C', null, fakeSymbolInformation(new Range(6, 4, 9, 4))); + + let group = new OutlineGroup('group', null, null, 1); + group.children[p.id] = p; + p.children[c1.id] = c1; + p.children[c2.id] = c2; + + let data = [ + fakeMarker(new Range(2, 4, 5, 4)) + ]; + + group.updateMarker(data); + assert.equal(p.marker.count, 0); + assert.equal(c1.marker.count, 1); + assert.equal(c2.marker, undefined); + + data = [ + fakeMarker(new Range(2, 4, 5, 4)), + fakeMarker(new Range(2, 6, 2, 8)), + fakeMarker(new Range(7, 6, 7, 8)), + ]; + group.updateMarker(data); + assert.equal(p.marker.count, 0); + assert.equal(c1.marker.count, 2); + assert.equal(c2.marker.count, 1); + + data = [ + fakeMarker(new Range(1, 4, 1, 11)), + fakeMarker(new Range(7, 6, 7, 8)), + ]; + group.updateMarker(data); + assert.equal(p.marker.count, 1); + assert.equal(c1.marker, undefined); + assert.equal(c2.marker.count, 1); + }); + + test('OutlineElement - updateMarker/multiple groups', function () { + + let model = new class extends OutlineModel { + constructor() { + super(null); + } + readyForTesting() { + this._groups = this.children as any; + } + }; + model.children['g1'] = new OutlineGroup('g1', model, null, 1); + model.children['g1'].children['c1'] = new OutlineElement('c1', model.children['g1'], fakeSymbolInformation(new Range(1, 1, 11, 1))); + + model.children['g2'] = new OutlineGroup('g2', model, null, 1); + model.children['g2'].children['c2'] = new OutlineElement('c2', model.children['g2'], fakeSymbolInformation(new Range(1, 1, 7, 1))); + model.children['g2'].children['c2'].children['c2.1'] = new OutlineElement('c2.1', model.children['g2'].children['c2'], fakeSymbolInformation(new Range(1, 3, 2, 19))); + model.children['g2'].children['c2'].children['c2.2'] = new OutlineElement('c2.2', model.children['g2'].children['c2'], fakeSymbolInformation(new Range(4, 1, 6, 10))); + + model.readyForTesting(); + + const data = [ + fakeMarker(new Range(1, 1, 2, 8)), + fakeMarker(new Range(6, 1, 6, 98)), + ]; + + model.updateMarker(data); + + assert.equal(model.children['g1'].children['c1'].marker.count, 2); + assert.equal(model.children['g2'].children['c2'].children['c2.1'].marker.count, 1); + assert.equal(model.children['g2'].children['c2'].children['c2.2'].marker.count, 1); + }); + +}); diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 103d4ca4370..f7f0d9f2286 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -7,11 +7,11 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import * as strings from 'vs/base/common/strings'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorContribution, registerEditorAction, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding, ToggleSearchScopeKeybinding, ShowPreviousFindTermKeybinding, ShowNextFindTermKeybinding, CONTEXT_FIND_WIDGET_VISIBLE, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; +import { FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding, ToggleSearchScopeKeybinding, CONTEXT_FIND_WIDGET_VISIBLE } from 'vs/editor/contrib/find/findModel'; import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; import { Delayer } from 'vs/base/common/async'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -23,10 +23,9 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { FindWidget, IFindController } from 'vs/editor/contrib/find/findWidget'; import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { showDeprecatedWarning } from 'vs/platform/widget/browser/input'; +import { MenuId } from 'vs/platform/actions/common/actions'; export function getSelectionSearchString(editor: ICodeEditor): string { let selection = editor.getSelection(); @@ -185,14 +184,17 @@ export class CommonFindController extends Disposable implements editorCommon.IEd public toggleCaseSensitive(): void { this._state.change({ matchCase: !this._state.matchCase }, false); + this.highlightFindOptions(); } public toggleWholeWords(): void { this._state.change({ wholeWord: !this._state.wholeWord }, false); + this.highlightFindOptions(); } public toggleRegex(): void { this._state.change({ isRegex: !this._state.isRegex }, false); + this.highlightFindOptions(); } public toggleSearchScope(): void { @@ -310,16 +312,6 @@ export class CommonFindController extends Disposable implements editorCommon.IEd return false; } - public showPreviousFindTerm(): boolean { - // overwritten in subclass - return false; - } - - public showNextFindTerm(): boolean { - // overwritten in subclass - return false; - } - public getGlobalBufferTerm(): string { if (this._editor.getConfiguration().contribInfo.find.globalFindClipboard && this._clipboardService @@ -352,7 +344,6 @@ export class FindController extends CommonFindController implements IFindControl @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, @IStorageService storageService: IStorageService, - @INotificationService private readonly _notificationService: INotificationService, @optional(IClipboardService) clipboardService: IClipboardService ) { super(editor, _contextKeyService, storageService, clipboardService); @@ -387,18 +378,6 @@ export class FindController extends CommonFindController implements IFindControl this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService, this._themeService)); } - - public showPreviousFindTerm(): boolean { - showDeprecatedWarning(this._notificationService, this._keybindingService, this._storageService); - this._widget.showPreviousFindTerm(); - return true; - } - - public showNextFindTerm(): boolean { - showDeprecatedWarning(this._notificationService, this._keybindingService, this._storageService); - this._widget.showNextFindTerm(); - return true; - } } export class StartFindAction extends EditorAction { @@ -411,7 +390,14 @@ export class StartFindAction extends EditorAction { precondition: null, kbOpts: { kbExpr: null, - primary: KeyMod.CtrlCmd | KeyCode.KEY_F + primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '3_find', + title: nls.localize({ key: 'miFind', comment: ['&& denotes a mnemonic'] }, "&&Find"), + order: 1 } }); } @@ -443,7 +429,8 @@ export class StartFindWithSelectionAction extends EditorAction { primary: null, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_E, - } + }, + weight: KeybindingWeight.EditorContrib } }); } @@ -492,7 +479,8 @@ export class NextMatchFindAction extends MatchFindAction { kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] } + mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }, + weight: KeybindingWeight.EditorContrib } }); } @@ -513,7 +501,8 @@ export class PreviousMatchFindAction extends MatchFindAction { kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] } + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, + weight: KeybindingWeight.EditorContrib } }); } @@ -558,7 +547,8 @@ export class NextSelectionMatchFindAction extends SelectionMatchFindAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.F3 + primary: KeyMod.CtrlCmd | KeyCode.F3, + weight: KeybindingWeight.EditorContrib } }); } @@ -578,7 +568,8 @@ export class PreviousSelectionMatchFindAction extends SelectionMatchFindAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F3 + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F3, + weight: KeybindingWeight.EditorContrib } }); } @@ -599,7 +590,14 @@ export class StartFindReplaceAction extends EditorAction { kbOpts: { kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_H, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_F } + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_F }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '3_find', + title: nls.localize({ key: 'miReplace', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + order: 2 } }); } @@ -614,10 +612,9 @@ export class StartFindReplaceAction extends EditorAction { // we only seed search string from selection when the current selection is single line and not empty. let seedSearchStringFromSelection = !currentSelection.isEmpty() && currentSelection.startLineNumber === currentSelection.endLineNumber && editor.getConfiguration().contribInfo.find.seedSearchStringFromSelection; - let oldSearchString = controller.getState().searchString; // if the existing search string in find widget is empty and we don't seed search string from selection, it means the Find Input // is still empty, so we should focus the Find Input instead of Replace Input. - let shouldFocus = (!!oldSearchString || seedSearchStringFromSelection) ? + let shouldFocus = seedSearchStringFromSelection ? FindStartFocusAction.FocusReplaceInput : FindStartFocusAction.FocusFindInput; if (controller) { @@ -632,54 +629,6 @@ export class StartFindReplaceAction extends EditorAction { } } -export class ShowNextFindTermAction extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.ShowNextFindTermAction, - label: nls.localize('showNextFindTermAction', "Show Next Find Term"), - alias: 'Show Next Find Term', - precondition: CONTEXT_FIND_WIDGET_VISIBLE, - kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), - kbExpr: ContextKeyExpr.and(CONTEXT_FIND_INPUT_FOCUSED, EditorContextKeys.focus), - primary: ShowNextFindTermKeybinding.primary, - mac: ShowNextFindTermKeybinding.mac, - win: ShowNextFindTermKeybinding.win, - linux: ShowNextFindTermKeybinding.linux - } - }); - } - - protected _run(controller: CommonFindController): boolean { - return controller.showNextFindTerm(); - } -} - -export class ShowPreviousFindTermAction extends MatchFindAction { - - constructor() { - super({ - id: FIND_IDS.ShowPreviousFindTermAction, - label: nls.localize('showPreviousFindTermAction', "Show Previous Find Term"), - alias: 'Find Show Previous Find Term', - precondition: CONTEXT_FIND_WIDGET_VISIBLE, - kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), - kbExpr: ContextKeyExpr.and(CONTEXT_FIND_INPUT_FOCUSED, EditorContextKeys.focus), - primary: ShowPreviousFindTermKeybinding.primary, - mac: ShowPreviousFindTermKeybinding.mac, - win: ShowPreviousFindTermKeybinding.win, - linux: ShowPreviousFindTermKeybinding.linux - } - }); - } - - protected _run(controller: CommonFindController): boolean { - return controller.showPreviousFindTerm(); - } -} - registerEditorContribution(FindController); registerEditorAction(StartFindAction); @@ -689,8 +638,6 @@ registerEditorAction(PreviousMatchFindAction); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); registerEditorAction(StartFindReplaceAction); -registerEditorAction(ShowNextFindTermAction); -registerEditorAction(ShowPreviousFindTermAction); const FindCommand = EditorCommand.bindToContribution(CommonFindController.get); @@ -699,7 +646,7 @@ registerEditorCommand(new FindCommand({ precondition: CONTEXT_FIND_WIDGET_VISIBLE, handler: x => x.closeFindWidget(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] @@ -711,7 +658,7 @@ registerEditorCommand(new FindCommand({ precondition: null, handler: x => x.toggleCaseSensitive(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: ToggleCaseSensitiveKeybinding.primary, mac: ToggleCaseSensitiveKeybinding.mac, @@ -725,7 +672,7 @@ registerEditorCommand(new FindCommand({ precondition: null, handler: x => x.toggleWholeWords(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: ToggleWholeWordKeybinding.primary, mac: ToggleWholeWordKeybinding.mac, @@ -739,7 +686,7 @@ registerEditorCommand(new FindCommand({ precondition: null, handler: x => x.toggleRegex(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: ToggleRegexKeybinding.primary, mac: ToggleRegexKeybinding.mac, @@ -753,7 +700,7 @@ registerEditorCommand(new FindCommand({ precondition: null, handler: x => x.toggleSearchScope(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: ToggleSearchScopeKeybinding.primary, mac: ToggleSearchScopeKeybinding.mac, @@ -767,7 +714,7 @@ registerEditorCommand(new FindCommand({ precondition: CONTEXT_FIND_WIDGET_VISIBLE, handler: x => x.replace(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_1 } @@ -778,7 +725,7 @@ registerEditorCommand(new FindCommand({ precondition: CONTEXT_FIND_WIDGET_VISIBLE, handler: x => x.replaceAll(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter } @@ -789,7 +736,7 @@ registerEditorCommand(new FindCommand({ precondition: CONTEXT_FIND_WIDGET_VISIBLE, handler: x => x.selectAllMatches(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(5), + weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, primary: KeyMod.Alt | KeyCode.Enter } diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 1196fbbd9bd..bf51ac38ce5 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -46,12 +46,6 @@ export const ToggleSearchScopeKeybinding: IKeybindings = { primary: KeyMod.Alt | KeyCode.KEY_L, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } }; -export const ShowPreviousFindTermKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.UpArrow -}; -export const ShowNextFindTermKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.DownArrow -}; export const FIND_IDS = { StartFindAction: 'actions.find', @@ -68,11 +62,7 @@ export const FIND_IDS = { ToggleSearchScopeCommand: 'toggleFindInSelection', ReplaceOneAction: 'editor.action.replaceOne', ReplaceAllAction: 'editor.action.replaceAll', - SelectAllMatchesAction: 'editor.action.selectAllMatches', - ShowPreviousFindTermAction: 'find.history.showPrevious', - ShowNextFindTermAction: 'find.history.showNext', - ShowPreviousReplaceTermAction: 'replace.history.showPrevious', - ShowNextReplaceTermAction: 'replace.history.showNext' + SelectAllMatchesAction: 'editor.action.selectAllMatches' }; export const MATCHES_LIMIT = 19999; diff --git a/src/vs/editor/contrib/find/findOptionsWidget.ts b/src/vs/editor/contrib/find/findOptionsWidget.ts index fcf2d486513..3a23a72c56b 100644 --- a/src/vs/editor/contrib/find/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/findOptionsWidget.ts @@ -53,38 +53,38 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.caseSensitive = this._register(new CaseSensitiveCheckbox({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand), isChecked: this._state.matchCase, - onChange: (viaKeyboard) => { - this._state.change({ - matchCase: this.caseSensitive.checked - }, false); - }, inputActiveOptionBorder: inputActiveOptionBorderColor })); this._domNode.appendChild(this.caseSensitive.domNode); + this._register(this.caseSensitive.onChange(() => { + this._state.change({ + matchCase: this.caseSensitive.checked + }, false); + })); this.wholeWords = this._register(new WholeWordsCheckbox({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand), isChecked: this._state.wholeWord, - onChange: (viaKeyboard) => { - this._state.change({ - wholeWord: this.wholeWords.checked - }, false); - }, inputActiveOptionBorder: inputActiveOptionBorderColor })); this._domNode.appendChild(this.wholeWords.domNode); + this._register(this.wholeWords.onChange(() => { + this._state.change({ + wholeWord: this.wholeWords.checked + }, false); + })); this.regex = this._register(new RegexCheckbox({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand), isChecked: this._state.isRegex, - onChange: (viaKeyboard) => { - this._state.change({ - isRegex: this.regex.checked - }, false); - }, inputActiveOptionBorder: inputActiveOptionBorderColor })); this._domNode.appendChild(this.regex.domNode); + this._register(this.regex.onChange(() => { + this._state.change({ + isRegex: this.regex.checked + }, false); + })); this._editor.addOverlayWidget(this); diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 82b25e526c5..51a7e330944 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -29,8 +29,9 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { ITheme, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; -import { editorFindRangeHighlight, editorFindMatch, editorFindMatchHighlight, contrastBorder, inputBackground, editorWidgetBackground, inputActiveOptionBorder, widgetShadow, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder, errorForeground, editorWidgetBorder, editorFindMatchBorder, editorFindMatchHighlightBorder, editorFindRangeHighlightBorder, editorWidgetResizeBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/input'; +import { editorFindRangeHighlight, editorFindMatch, editorFindMatchHighlight, contrastBorder, inputBackground, editorWidgetBackground, inputActiveOptionBorder, widgetShadow, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, errorForeground, editorWidgetBorder, editorFindMatchBorder, editorFindMatchHighlightBorder, editorFindRangeHighlightBorder, editorWidgetResizeBorder } from 'vs/platform/theme/common/colorRegistry'; +import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import { toDisposable } from 'vs/base/common/lifecycle'; export interface IFindController { @@ -106,6 +107,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _isVisible: boolean; private _isReplaceVisible: boolean; + private _ignoreChangeEvent: boolean; private _findFocusTracker: dom.IFocusTracker; private _findInputFocused: IContextKey; @@ -137,8 +139,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._isVisible = false; this._isReplaceVisible = false; + this._ignoreChangeEvent = false; this._updateHistoryDelayer = new Delayer(500); + this._register(toDisposable(() => this._updateHistoryDelayer.cancel())); this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e))); this._buildDomNode(); this._updateButtons(); @@ -244,19 +248,16 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas return null; } - public showNextFindTerm() { - this._findInput.inputBox.showNextValue(); - } - - public showPreviousFindTerm() { - this._findInput.inputBox.showPreviousValue(); - } - // ----- React to state changes private _onStateChanged(e: FindReplaceStateChangedEvent): void { if (e.searchString) { - this._findInput.setValue(this._state.searchString); + try { + this._ignoreChangeEvent = true; + this._findInput.setValue(this._state.searchString); + } finally { + this._ignoreChangeEvent = false; + } this._updateButtons(); } if (e.replaceString) { @@ -320,10 +321,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _updateHistory() { if (this._state.searchString) { - this._findInput.inputBox.addToHistory(this._state.searchString); + this._findInput.inputBox.addToHistory(); } if (this._state.replaceString) { - this._replaceInputBox.addToHistory(this._state.replaceString); + this._replaceInputBox.addToHistory(); } } @@ -412,6 +413,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas dom.addClass(this._domNode, 'visible'); this._domNode.setAttribute('aria-hidden', 'false'); }, 0); + + // validate query again as it's being dismissed when we hide the find widget. + setTimeout(() => { + this._findInput.validate(); + }, 200); + this._codeEditor.layoutOverlayWidget(this); let adjustEditorScrollTop = true; @@ -449,6 +456,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas dom.removeClass(this._domNode, 'visible'); this._domNode.setAttribute('aria-hidden', 'true'); + this._findInput.clearMessage(); if (focusTheEditor) { this._codeEditor.focus(); } @@ -520,10 +528,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas inputForeground: theme.getColor(inputForeground), inputBorder: theme.getColor(inputBorder), inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) }; this._findInput.style(inputStyles); @@ -621,13 +632,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().done(null, onUnexpectedError); + this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().then(null, onUnexpectedError); e.preventDefault(); return; } if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().done(null, onUnexpectedError); + this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().then(null, onUnexpectedError); e.preventDefault(); return; } @@ -734,6 +745,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._findInput.setWholeWords(!!this._state.wholeWord); this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e))); this._register(this._findInput.inputBox.onDidChange(() => { + if (this._ignoreChangeEvent) { + return; + } this._state.change({ searchString: this._findInput.getValue() }, true); })); this._register(this._findInput.onDidOptionChange(() => { @@ -764,7 +778,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.PreviousMatchFindAction), className: 'previous', onTrigger: () => { - this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().done(null, onUnexpectedError); + this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().then(null, onUnexpectedError); } })); @@ -773,7 +787,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.NextMatchFindAction), className: 'next', onTrigger: () => { - this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().done(null, onUnexpectedError); + this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().then(null, onUnexpectedError); } })); @@ -840,8 +854,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas history: [] }, this._contextKeyService)); + this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'keydown', (e) => this._onReplaceInputKeyDown(e))); - this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'input', (e) => { + this._register(this._replaceInputBox.onDidChange((e) => { this._state.change({ replaceString: this._replaceInputBox.value }, false); })); diff --git a/src/vs/editor/contrib/find/simpleFindWidget.ts b/src/vs/editor/contrib/find/simpleFindWidget.ts index 44f7066321e..f99159eeb65 100644 --- a/src/vs/editor/contrib/find/simpleFindWidget.ts +++ b/src/vs/editor/contrib/find/simpleFindWidget.ts @@ -12,13 +12,10 @@ import * as dom from 'vs/base/browser/dom'; import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService'; -import { inputBackground, inputActiveOptionBorder, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputActiveOptionBorder, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { SimpleButton } from './findWidget'; -import { ContextScopedFindInput, showDeprecatedWarning } from 'vs/platform/widget/browser/input'; +import { ContextScopedFindInput } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -37,10 +34,7 @@ export abstract class SimpleFindWidget extends Widget { constructor( @IContextViewService private readonly _contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @INotificationService private readonly _notificationService: INotificationService, - @IStorageService private readonly _storageService: IStorageService + @IContextKeyService contextKeyService: IContextKeyService ) { super(); @@ -139,6 +133,10 @@ export abstract class SimpleFindWidget extends Widget { return this._findInput.getValue(); } + public get focusTracker(): dom.IFocusTracker { + return this._findInputFocusTracker; + } + public updateTheme(theme: ITheme): void { const inputStyles = { inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), @@ -146,10 +144,13 @@ export abstract class SimpleFindWidget extends Widget { inputForeground: theme.getColor(inputForeground), inputBorder: theme.getColor(inputBorder), inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) }; this._findInput.style(inputStyles); @@ -160,6 +161,7 @@ export abstract class SimpleFindWidget extends Widget { if (this._domNode && this._domNode.parentElement) { this._domNode.parentElement.removeChild(this._domNode); + this._domNode = undefined; } } @@ -202,19 +204,7 @@ export abstract class SimpleFindWidget extends Widget { } protected _updateHistory() { - if (this.inputValue) { - this._findInput.inputBox.addToHistory(this._findInput.getValue()); - } - } - - public showNextFindTerm() { - showDeprecatedWarning(this._notificationService, this._keybindingService, this._storageService); - this._findInput.inputBox.showNextValue(); - } - - public showPreviousFindTerm() { - showDeprecatedWarning(this._notificationService, this._keybindingService, this._storageService); - this._findInput.inputBox.showPreviousValue(); + this._findInput.inputBox.addToHistory(); } } diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index d614610198b..0af5f01951f 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -9,7 +9,7 @@ import 'vs/css!./folding'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { RunOnceScheduler, Delayer, asWinJsPromise } from 'vs/base/common/async'; +import { RunOnceScheduler, Delayer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -29,9 +29,11 @@ import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageCo import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider'; import { IPosition } from 'vs/editor/common/core/position'; import { FoldingRangeProviderRegistry, FoldingRangeKind } from 'vs/editor/common/modes'; -import { SyntaxRangeProvider } from './syntaxRangeProvider'; +import { SyntaxRangeProvider, ID_SYNTAX_PROVIDER } from './syntaxRangeProvider'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { InitializingRangeProvider } from 'vs/editor/contrib/folding/intializingRangeProvider'; +import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/folding/intializingRangeProvider'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { onUnexpectedError } from 'vs/base/common/errors'; export const ID = 'editor.contrib.folding'; @@ -67,7 +69,7 @@ export class FoldingController implements IEditorContribution { private hiddenRangeModel: HiddenRangeModel; private rangeProvider: RangeProvider; - private foldingRegionPromise: TPromise; + private foldingRegionPromise: CancelablePromise; private foldingStateMemento: FoldingStateMemento; @@ -155,7 +157,7 @@ export class FoldingController implements IEditorContribution { return; } - if (state.provider !== 'indent') { + if (state.provider === ID_SYNTAX_PROVIDER || state.provider === ID_INIT_PROVIDER) { this.foldingStateMemento = state; } @@ -165,7 +167,7 @@ export class FoldingController implements IEditorContribution { if (foldingModel) { foldingModel.applyMemento(state.collapsedRegions); } - }); + }).then(undefined, onUnexpectedError); } } @@ -261,8 +263,8 @@ export class FoldingController implements IEditorContribution { if (!this.foldingModel) { // null if editor has been disposed, or folding turned off return null; } - let foldingRegionPromise = this.foldingRegionPromise = asWinJsPromise(token => this.getRangeProvider(this.foldingModel.textModel).compute(token)); - return foldingRegionPromise.then(foldingRanges => { + let foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => this.getRangeProvider(this.foldingModel.textModel).compute(token)); + return TPromise.wrap(foldingRegionPromise.then(foldingRanges => { if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime? // some cursors might have moved into hidden regions, make sure they are in expanded regions let selections = this.editor.getSelections(); @@ -270,7 +272,7 @@ export class FoldingController implements IEditorContribution { this.foldingModel.update(foldingRanges, selectionLineNumbers); } return this.foldingModel; - }); + })); }); } } @@ -311,7 +313,7 @@ export class FoldingController implements IEditorContribution { } } } - }); + }).then(undefined, onUnexpectedError); } @@ -331,10 +333,13 @@ export class FoldingController implements IEditorContribution { switch (e.target.type) { case MouseTargetType.GUTTER_LINE_DECORATIONS: const data = e.target.detail as IMarginData; - const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; + const offsetLeftInGutter = (e.target.element as HTMLElement).offsetLeft; + const gutterOffsetX = data.offsetX - offsetLeftInGutter; + + // const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; // TODO@joao TODO@alex TODO@martin this is such that we don't collide with dirty diff - if (gutterOffsetX <= 10) { + if (gutterOffsetX < 5) { // the whitespace between the border and the real folding icon border is 5px return; } @@ -403,7 +408,7 @@ export class FoldingController implements IEditorContribution { } } } - }); + }).then(undefined, onUnexpectedError); } public reveal(position: IPosition): void { @@ -486,7 +491,8 @@ class UnfoldAction extends FoldingAction { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET - } + }, + weight: KeybindingWeight.EditorContrib }, description: { description: 'Unfold the content in the editor', @@ -495,7 +501,7 @@ class UnfoldAction extends FoldingAction { name: 'Unfold editor argument', description: `Property-value pairs that can be passed through this argument: * 'levels': Number of levels to unfold. If not set, defaults to 1. - * 'direction': If 'up', unfold given number of levels up otherwise unfolds down + * 'direction': If 'up', unfold given number of levels up otherwise unfolds down. * 'selectionLines': The start lines (0-based) of the editor selections to apply the unfold action to. If not set, the active selection(s) will be used. `, constraint: foldingArgumentsConstraint @@ -526,7 +532,8 @@ class UnFoldRecursivelyAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET), + weight: KeybindingWeight.EditorContrib } }); } @@ -549,7 +556,8 @@ class FoldAction extends FoldingAction { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET - } + }, + weight: KeybindingWeight.EditorContrib }, description: { description: 'Fold the content in the editor', @@ -557,8 +565,8 @@ class FoldAction extends FoldingAction { { name: 'Fold editor argument', description: `Property-value pairs that can be passed through this argument: - * 'levels': Number of levels to fold. Defaults to 1 - * 'direction': If 'up', folds given number of levels up otherwise folds down + * 'levels': Number of levels to fold. Defaults to 1. + * 'direction': If 'up', folds given number of levels up otherwise folds down. * 'selectionLines': The start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used. `, constraint: foldingArgumentsConstraint @@ -589,7 +597,8 @@ class FoldRecursivelyAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET), + weight: KeybindingWeight.EditorContrib } }); } @@ -610,7 +619,8 @@ class FoldAllBlockCommentsAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_SLASH) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_SLASH), + weight: KeybindingWeight.EditorContrib } }); } @@ -638,7 +648,8 @@ class FoldAllRegionsAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_8) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_8), + weight: KeybindingWeight.EditorContrib } }); } @@ -666,7 +677,8 @@ class UnfoldAllRegionsAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_9) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_9), + weight: KeybindingWeight.EditorContrib } }); } @@ -694,7 +706,8 @@ class FoldAllAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_0) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_0), + weight: KeybindingWeight.EditorContrib } }); } @@ -714,7 +727,8 @@ class UnfoldAllAction extends FoldingAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_J) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_J), + weight: KeybindingWeight.EditorContrib } }); } @@ -757,7 +771,8 @@ for (let i = 1; i <= 7; i++) { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | (KeyCode.KEY_0 + i)) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | (KeyCode.KEY_0 + i)), + weight: KeybindingWeight.EditorContrib } }) ); diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index d897e5193da..727bcd29de1 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -16,8 +16,10 @@ import { CancellationToken } from 'vs/base/common/cancellation'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; +export const ID_INDENT_PROVIDER = 'indent'; + export class IndentRangeProvider implements RangeProvider { - readonly id = 'indent'; + readonly id = ID_INDENT_PROVIDER; readonly decorations; diff --git a/src/vs/editor/contrib/folding/intializingRangeProvider.ts b/src/vs/editor/contrib/folding/intializingRangeProvider.ts index 4e7e024e80d..bd5fa4ae50b 100644 --- a/src/vs/editor/contrib/folding/intializingRangeProvider.ts +++ b/src/vs/editor/contrib/folding/intializingRangeProvider.ts @@ -12,8 +12,10 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; +export const ID_INIT_PROVIDER = 'init'; + export class InitializingRangeProvider implements RangeProvider { - readonly id = 'init'; + readonly id = ID_INIT_PROVIDER; private decorationIds: string[] | undefined; private timeout: number; diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index 916fc5bfb65..e52d226638a 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -23,9 +23,11 @@ export interface IFoldingRangeData extends FoldingRange { const foldingContext: FoldingContext = { }; +export const ID_SYNTAX_PROVIDER = 'syntax'; + export class SyntaxRangeProvider implements RangeProvider { - readonly id = 'syntax'; + readonly id = ID_SYNTAX_PROVIDER; constructor(private editorModel: ITextModel, private providers: FoldingRangeProvider[], private limit = MAX_FOLDING_REGIONS) { } diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index bc6ed0f89d9..ea25a1e2086 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -3,19 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand, registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { DocumentFormattingEditProviderRegistry, DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry, FormattingOptions, TextEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { asWinJsPromise, sequence } from 'vs/base/common/async'; +import { first2 } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class NoProviderError extends Error { @@ -28,60 +26,44 @@ export class NoProviderError extends Error { } } -export function getDocumentRangeFormattingEdits(model: ITextModel, range: Range, options: FormattingOptions): TPromise { +export function getDocumentRangeFormattingEdits(model: ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): Promise { const providers = DocumentRangeFormattingEditProviderRegistry.ordered(model); if (providers.length === 0) { - return TPromise.wrapError(new NoProviderError()); + return Promise.reject(new NoProviderError()); } - let result: TextEdit[]; - return sequence(providers.map(provider => { - return () => { - if (!isFalsyOrEmpty(result)) { - return undefined; - } - return asWinJsPromise(token => provider.provideDocumentRangeFormattingEdits(model, range, options, token)).then(value => { - result = value; - }, onUnexpectedExternalError); - }; - })).then(() => result); + return first2(providers.map(provider => () => { + return Promise.resolve(provider.provideDocumentRangeFormattingEdits(model, range, options, token)) + .then(undefined, onUnexpectedExternalError); + }), result => !isFalsyOrEmpty(result)); } -export function getDocumentFormattingEdits(model: ITextModel, options: FormattingOptions): TPromise { +export function getDocumentFormattingEdits(model: ITextModel, options: FormattingOptions, token: CancellationToken): Promise { const providers = DocumentFormattingEditProviderRegistry.ordered(model); // try range formatters when no document formatter is registered if (providers.length === 0) { - return getDocumentRangeFormattingEdits(model, model.getFullModelRange(), options); + return getDocumentRangeFormattingEdits(model, model.getFullModelRange(), options, token); } - let result: TextEdit[]; - return sequence(providers.map(provider => { - return () => { - if (!isFalsyOrEmpty(result)) { - return undefined; - } - return asWinJsPromise(token => provider.provideDocumentFormattingEdits(model, options, token)).then(value => { - result = value; - }, onUnexpectedExternalError); - }; - })).then(() => result); + return first2(providers.map(provider => () => { + return Promise.resolve(provider.provideDocumentFormattingEdits(model, options, token)) + .then(undefined, onUnexpectedExternalError); + }), result => !isFalsyOrEmpty(result)); } -export function getOnTypeFormattingEdits(model: ITextModel, position: Position, ch: string, options: FormattingOptions): TPromise { +export function getOnTypeFormattingEdits(model: ITextModel, position: Position, ch: string, options: FormattingOptions): Promise { const [support] = OnTypeFormattingEditProviderRegistry.ordered(model); if (!support) { - return TPromise.as(undefined); + return Promise.resolve(undefined); } if (support.autoFormatTriggerCharacters.indexOf(ch) < 0) { - return TPromise.as(undefined); + return Promise.resolve(undefined); } - return asWinJsPromise((token) => { - return support.provideOnTypeFormattingEdits(model, position, ch, options, token); - }).then(r => r, onUnexpectedExternalError); + return Promise.resolve(support.provideOnTypeFormattingEdits(model, position, ch, options, CancellationToken.None)).then(r => r, onUnexpectedExternalError); } registerLanguageCommand('_executeFormatRangeProvider', function (accessor, args) { @@ -93,7 +75,7 @@ registerLanguageCommand('_executeFormatRangeProvider', function (accessor, args) if (!model) { throw illegalArgument('resource'); } - return getDocumentRangeFormattingEdits(model, Range.lift(range), options); + return getDocumentRangeFormattingEdits(model, Range.lift(range), options, CancellationToken.None); }); registerLanguageCommand('_executeFormatDocumentProvider', function (accessor, args) { @@ -106,7 +88,7 @@ registerLanguageCommand('_executeFormatDocumentProvider', function (accessor, ar throw illegalArgument('resource'); } - return getDocumentFormattingEdits(model, options); + return getDocumentFormattingEdits(model, options, CancellationToken.None); }); registerDefaultLanguageCommand('_executeFormatOnTypeProvider', function (model, position, args) { diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 985dd3e900e..6d425548f9d 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -26,6 +26,8 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ISingleEditOperation } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { CancellationToken } from 'vs/base/common/cancellation'; function alertFormattingEdits(edits: ISingleEditOperation[]): void { @@ -238,7 +240,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution { const { tabSize, insertSpaces } = model.getOptions(); const state = new EditorState(this.editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); - getDocumentRangeFormattingEdits(model, range, { tabSize, insertSpaces }).then(edits => { + getDocumentRangeFormattingEdits(model, range, { tabSize, insertSpaces }, CancellationToken.None).then(edits => { return this.workerService.computeMoreMinimalEdits(model.uri, edits); }).then(edits => { if (!state.validate(this.editor) || isFalsyOrEmpty(edits)) { @@ -266,7 +268,7 @@ export abstract class AbstractFormatAction extends EditorAction { const workerService = accessor.get(IEditorWorkerService); const notificationService = accessor.get(INotificationService); - const formattingPromise = this._getFormattingEdits(editor); + const formattingPromise = this._getFormattingEdits(editor, CancellationToken.None); if (!formattingPromise) { return TPromise.as(void 0); } @@ -275,7 +277,7 @@ export abstract class AbstractFormatAction extends EditorAction { const state = new EditorState(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); // Receive formatted value from worker - return formattingPromise.then(edits => workerService.computeMoreMinimalEdits(editor.getModel().uri, edits)).then(edits => { + return TPromise.wrap(formattingPromise).then(edits => workerService.computeMoreMinimalEdits(editor.getModel().uri, edits)).then(edits => { if (!state.validate(editor) || isFalsyOrEmpty(edits)) { return; } @@ -283,6 +285,7 @@ export abstract class AbstractFormatAction extends EditorAction { FormattingEdit.execute(editor, edits); alertFormattingEdits(edits); editor.focus(); + editor.revealPositionInCenterIfOutsideViewport(editor.getPosition(), editorCommon.ScrollType.Immediate); }, err => { if (err instanceof Error && err.name === NoProviderError.Name) { this._notifyNoProviderError(notificationService, editor.getModel().getLanguageIdentifier().language); @@ -292,7 +295,8 @@ export abstract class AbstractFormatAction extends EditorAction { }); } - protected abstract _getFormattingEdits(editor: ICodeEditor): TPromise; + protected abstract _getFormattingEdits(editor: ICodeEditor, token: CancellationToken): Promise; + protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { notificationService.info(nls.localize('no.provider', "There is no formatter for '{0}'-files installed.", language)); } @@ -310,7 +314,8 @@ export class FormatDocumentAction extends AbstractFormatAction { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, // secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D)], - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I } + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, + weight: KeybindingWeight.EditorContrib }, menuOpts: { when: EditorContextKeys.hasDocumentFormattingProvider, @@ -320,10 +325,10 @@ export class FormatDocumentAction extends AbstractFormatAction { }); } - protected _getFormattingEdits(editor: ICodeEditor): TPromise { + protected _getFormattingEdits(editor: ICodeEditor, token: CancellationToken): Promise { const model = editor.getModel(); const { tabSize, insertSpaces } = model.getOptions(); - return getDocumentFormattingEdits(model, { tabSize, insertSpaces }); + return getDocumentFormattingEdits(model, { tabSize, insertSpaces }, token); } protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { @@ -341,7 +346,8 @@ export class FormatSelectionAction extends AbstractFormatAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F), + weight: KeybindingWeight.EditorContrib }, menuOpts: { when: ContextKeyExpr.and(EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasNonEmptySelection), @@ -351,10 +357,10 @@ export class FormatSelectionAction extends AbstractFormatAction { }); } - protected _getFormattingEdits(editor: ICodeEditor): TPromise { + protected _getFormattingEdits(editor: ICodeEditor, token: CancellationToken): Promise { const model = editor.getModel(); const { tabSize, insertSpaces } = model.getOptions(); - return getDocumentRangeFormattingEdits(model, editor.getSelection(), { tabSize, insertSpaces }); + return getDocumentRangeFormattingEdits(model, editor.getSelection(), { tabSize, insertSpaces }, token); } protected _notifyNoProviderError(notificationService: INotificationService, language: string): void { @@ -376,14 +382,14 @@ CommandsRegistry.registerCommand('editor.action.format', accessor => { constructor() { super({} as IActionOptions); } - _getFormattingEdits(editor: ICodeEditor): TPromise { + _getFormattingEdits(editor: ICodeEditor, token: CancellationToken): Promise { const model = editor.getModel(); const editorSelection = editor.getSelection(); const { tabSize, insertSpaces } = model.getOptions(); return editorSelection.isEmpty() - ? getDocumentFormattingEdits(model, { tabSize, insertSpaces }) - : getDocumentRangeFormattingEdits(model, editorSelection, { tabSize, insertSpaces }); + ? getDocumentFormattingEdits(model, { tabSize, insertSpaces }, token) + : getDocumentRangeFormattingEdits(model, editorSelection, { tabSize, insertSpaces }, token); } }().run(accessor, editor); } diff --git a/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts b/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts index f8157b3bf0c..8bc694f358a 100644 --- a/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts +++ b/src/vs/editor/contrib/goToDefinition/clickLinkGesture.ts @@ -52,19 +52,20 @@ export class ClickLinkKeyboardEvent { this.hasTriggerModifier = hasModifier(source, opts.triggerModifier); } } +export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; export class ClickLinkOptions { public readonly triggerKey: KeyCode; - public readonly triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; + public readonly triggerModifier: TriggerModifier; public readonly triggerSideBySideKey: KeyCode; - public readonly triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'; + public readonly triggerSideBySideModifier: TriggerModifier; constructor( triggerKey: KeyCode, - triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey', + triggerModifier: TriggerModifier, triggerSideBySideKey: KeyCode, - triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey' + triggerSideBySideModifier: TriggerModifier ) { this.triggerKey = triggerKey; this.triggerModifier = triggerModifier; diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts index f79912d8323..e5fc9e10930 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts @@ -3,60 +3,55 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { ITextModel } from 'vs/editor/common/model'; -import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; -import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; -import { DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry, Location } from 'vs/editor/common/modes'; +import { flatten, coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; -import { flatten } from 'vs/base/common/arrays'; +import { ITextModel } from 'vs/editor/common/model'; +import { DefinitionLink, DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry } from 'vs/editor/common/modes'; +import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; + function getDefinitions( model: ITextModel, position: Position, registry: LanguageFeatureRegistry, - provide: (provider: T, model: ITextModel, position: Position, token: CancellationToken) => Location | Location[] | Thenable -): TPromise { + provide: (provider: T, model: ITextModel, position: Position) => DefinitionLink | DefinitionLink[] | Thenable +): Thenable { const provider = registry.ordered(model); // get results - const promises = provider.map((provider, idx): TPromise => { - return asWinJsPromise((token) => { - return provide(provider, model, position, token); - }).then(undefined, err => { + const promises = provider.map((provider): Thenable => { + return Promise.resolve(provide(provider, model, position)).then(undefined, err => { onUnexpectedExternalError(err); return null; }); }); - return TPromise.join(promises) + return Promise.all(promises) .then(flatten) - .then(references => references.filter(x => !!x)); + .then(references => coalesce(references)); } -export function getDefinitionsAtPosition(model: ITextModel, position: Position): TPromise { - return getDefinitions(model, position, DefinitionProviderRegistry, (provider, model, position, token) => { +export function getDefinitionsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Thenable { + return getDefinitions(model, position, DefinitionProviderRegistry, (provider, model, position) => { return provider.provideDefinition(model, position, token); }); } -export function getImplementationsAtPosition(model: ITextModel, position: Position): TPromise { - return getDefinitions(model, position, ImplementationProviderRegistry, (provider, model, position, token) => { +export function getImplementationsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Thenable { + return getDefinitions(model, position, ImplementationProviderRegistry, (provider, model, position) => { return provider.provideImplementation(model, position, token); }); } -export function getTypeDefinitionsAtPosition(model: ITextModel, position: Position): TPromise { - return getDefinitions(model, position, TypeDefinitionProviderRegistry, (provider, model, position, token) => { +export function getTypeDefinitionsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Thenable { + return getDefinitions(model, position, TypeDefinitionProviderRegistry, (provider, model, position) => { return provider.provideTypeDefinition(model, position, token); }); } -registerDefaultLanguageCommand('_executeDefinitionProvider', getDefinitionsAtPosition); -registerDefaultLanguageCommand('_executeImplementationProvider', getImplementationsAtPosition); -registerDefaultLanguageCommand('_executeTypeDefinitionProvider', getTypeDefinitionsAtPosition); +registerDefaultLanguageCommand('_executeDefinitionProvider', (model, position) => getDefinitionsAtPosition(model, position, CancellationToken.None)); +registerDefaultLanguageCommand('_executeImplementationProvider', (model, position) => getImplementationsAtPosition(model, position, CancellationToken.None)); +registerDefaultLanguageCommand('_executeTypeDefinitionProvider', (model, position) => getTypeDefinitionsAtPosition(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts index 348b9833544..bd859bd3cde 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vs/nls'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; @@ -13,7 +11,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { registerEditorAction, IActionOptions, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; -import { Location } from 'vs/editor/common/modes'; +import { DefinitionLink } from 'vs/editor/common/modes'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from './goToDefinition'; import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/referencesModel'; @@ -26,6 +24,10 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { createCancelablePromise } from 'vs/base/common/async'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class DefinitionActionConfig { @@ -56,7 +58,7 @@ export class DefinitionAction extends EditorAction { const model = editor.getModel(); const pos = editor.getPosition(); - const definitionPromise = this._getDeclarationsAtPosition(model, pos).then(references => { + const definitionPromise = this._getDeclarationsAtPosition(model, pos, CancellationToken.None).then(references => { if (model.isDisposed() || editor.getModel() !== model) { // new model, no more model @@ -66,7 +68,7 @@ export class DefinitionAction extends EditorAction { // * remove falsy references // * find reference at the current pos let idxOfCurrent = -1; - let result: Location[] = []; + const result: DefinitionLink[] = []; for (let i = 0; i < references.length; i++) { let reference = references[i]; if (!reference || !reference.range) { @@ -108,11 +110,11 @@ export class DefinitionAction extends EditorAction { }); progressService.showWhile(definitionPromise, 250); - return definitionPromise; + return TPromise.wrap(definitionPromise); } - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { - return getDefinitionsAtPosition(model, position); + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position, token: CancellationToken): Thenable { + return getDefinitionsAtPosition(model, position, token); } protected _getNoResultFoundMessage(info?: IWordAtPosition): string { @@ -144,13 +146,13 @@ export class DefinitionAction extends EditorAction { } } - private _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: Location, sideBySide: boolean): TPromise { - let { uri, range } = reference; + private _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: DefinitionLink, sideBySide: boolean): TPromise { + const { uri, range } = reference; return editorService.openCodeEditor({ resource: uri, options: { selection: Range.collapseToStart(range), - revealIfVisible: true, + revealIfOpened: true, revealInCenterIfOutsideViewport: true } }, editor, sideBySide); @@ -159,7 +161,7 @@ export class DefinitionAction extends EditorAction { private _openInPeek(editorService: ICodeEditorService, target: ICodeEditor, model: ReferencesModel) { let controller = ReferencesController.get(target); if (controller) { - controller.toggleWidget(target.getSelection(), TPromise.as(model), { + controller.toggleWidget(target.getSelection(), createCancelablePromise(_ => Promise.resolve(model)), { getMetaTitle: (model) => { return this._getMetaTitle(model); }, @@ -192,7 +194,8 @@ export class GoToDefinitionAction extends DefinitionAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: goToDeclarationKb + primary: goToDeclarationKb, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: 'navigation', @@ -216,7 +219,8 @@ export class OpenDefinitionToSideAction extends DefinitionAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, goToDeclarationKb) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, goToDeclarationKb), + weight: KeybindingWeight.EditorContrib } }); } @@ -235,7 +239,8 @@ export class PeekDefinitionAction extends DefinitionAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F12, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F10 } + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F10 }, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: 'navigation', @@ -246,8 +251,8 @@ export class PeekDefinitionAction extends DefinitionAction { } export class ImplementationAction extends DefinitionAction { - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { - return getImplementationsAtPosition(model, position); + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position, token: CancellationToken): Thenable { + return getImplementationsAtPosition(model, position, token); } protected _getNoResultFoundMessage(info?: IWordAtPosition): string { @@ -275,7 +280,8 @@ export class GoToImplementationAction extends ImplementationAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.F12 + primary: KeyMod.CtrlCmd | KeyCode.F12, + weight: KeybindingWeight.EditorContrib } }); } @@ -295,15 +301,16 @@ export class PeekImplementationAction extends ImplementationAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F12 + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F12, + weight: KeybindingWeight.EditorContrib } }); } } export class TypeDefinitionAction extends DefinitionAction { - protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position): TPromise { - return getTypeDefinitionsAtPosition(model, position); + protected _getDeclarationsAtPosition(model: ITextModel, position: corePosition.Position, token: CancellationToken): Thenable { + return getTypeDefinitionsAtPosition(model, position, token); } protected _getNoResultFoundMessage(info?: IWordAtPosition): string { @@ -331,7 +338,8 @@ export class GoToTypeDefinitionAction extends TypeDefinitionAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: 0 + primary: 0, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: 'navigation', @@ -355,7 +363,8 @@ export class PeekTypeDefinitionAction extends TypeDefinitionAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: 0 + primary: 0, + weight: KeybindingWeight.EditorContrib } }); } @@ -368,3 +377,31 @@ registerEditorAction(GoToImplementationAction); registerEditorAction(PeekImplementationAction); registerEditorAction(GoToTypeDefinitionAction); registerEditorAction(PeekTypeDefinitionAction); + +// Go to menu +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToDeclaration', + title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToTypeDefinition', + title: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definition") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToImplementation', + title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementation") + }, + order: 6 +}); diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionMouse.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionMouse.ts index b57914b16d3..48b589f2ee3 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionMouse.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionMouse.ts @@ -7,14 +7,15 @@ import 'vs/css!./goToDefinitionMouse'; import * as nls from 'vs/nls'; -import { Throttler } from 'vs/base/common/async'; +import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { Location, DefinitionProviderRegistry } from 'vs/editor/common/modes'; +import { DefinitionProviderRegistry, DefinitionLink } from 'vs/editor/common/modes'; import { ICodeEditor, IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { getDefinitionsAtPosition } from './goToDefinition'; @@ -25,7 +26,8 @@ import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegist import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; import { DefinitionAction, DefinitionActionConfig } from './goToDefinitionCommands'; import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'vs/editor/contrib/goToDefinition/clickLinkGesture'; -import { IWordAtPosition, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IWordAtPosition, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { Position } from 'vs/editor/common/core/position'; class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorContribution { @@ -36,7 +38,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC private toUnhook: IDisposable[]; private decorations: string[]; private currentWordUnderMouse: IWordAtPosition; - private throttler: Throttler; + private previousPromise: CancelablePromise; constructor( editor: ICodeEditor, @@ -46,7 +48,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC this.toUnhook = []; this.decorations = []; this.editor = editor; - this.throttler = new Throttler(); + this.previousPromise = null; let linkGesture = new ClickLinkGesture(editor); this.toUnhook.push(linkGesture); @@ -57,7 +59,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC this.toUnhook.push(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => { if (this.isEnabled(mouseEvent)) { - this.gotoDefinition(mouseEvent.target, mouseEvent.hasSideBySideModifier).done(() => { + this.gotoDefinition(mouseEvent.target, mouseEvent.hasSideBySideModifier).then(() => { this.removeDecorations(); }, (error: Error) => { this.removeDecorations(); @@ -99,12 +101,14 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC // Find definition and decorate word if found let state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll); - this.throttler.queue(() => { - return state.validate(this.editor) - ? this.findDefinition(mouseEvent.target) - : TPromise.wrap(null); + if (this.previousPromise) { + this.previousPromise.cancel(); + this.previousPromise = null; + } - }).then(results => { + this.previousPromise = createCancelablePromise(token => this.findDefinition(mouseEvent.target, token)); + + this.previousPromise.then(results => { if (!results || !results.length || !state.validate(this.editor)) { this.removeDecorations(); return; @@ -136,35 +140,115 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC const { object: { textEditorModel } } = ref; const { startLineNumber } = result.range; - if (textEditorModel.getLineMaxColumn(startLineNumber) === 0) { + if (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) { + // invalid range ref.dispose(); return; } - const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber); - const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionWithMouseEditorContribution.MAX_SOURCE_PREVIEW_LINES); - let endLineNumber = startLineNumber + 1; - let minIndent = startIndent; + const previewValue = this.getPreviewValue(textEditorModel, startLineNumber); - for (; endLineNumber < maxLineNumber; endLineNumber++) { - let endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber); - minIndent = Math.min(minIndent, endIndent); - if (startIndent === endIndent) { - break; - } + let wordRange: Range; + if (result.origin) { + wordRange = Range.lift(result.origin); + } else { + wordRange = new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); } - const previewRange = new Range(startLineNumber, 1, endLineNumber + 1, 1); - const value = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim(); - this.addDecoration( - new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn), - new MarkdownString().appendCodeblock(this.modeService.getModeIdByFilenameOrFirstLine(textEditorModel.uri.fsPath), value) + wordRange, + new MarkdownString().appendCodeblock(this.modeService.getModeIdByFilenameOrFirstLine(textEditorModel.uri.fsPath), previewValue) ); ref.dispose(); }); } - }).done(undefined, onUnexpectedError); + }).then(undefined, onUnexpectedError); + } + + private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number) { + let rangeToUse = this.getPreviewRangeBasedOnBrackets(textEditorModel, startLineNumber); + const numberOfLinesInRange = rangeToUse.endLineNumber - rangeToUse.startLineNumber; + if (numberOfLinesInRange >= GotoDefinitionWithMouseEditorContribution.MAX_SOURCE_PREVIEW_LINES) { + rangeToUse = this.getPreviewRangeBasedOnIndentation(textEditorModel, startLineNumber); + } + + const previewValue = this.stripIndentationFromPreviewRange(textEditorModel, startLineNumber, rangeToUse); + return previewValue; + } + + private stripIndentationFromPreviewRange(textEditorModel: ITextModel, startLineNumber: number, previewRange: Range) { + const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber); + let minIndent = startIndent; + + for (let endLineNumber = startLineNumber + 1; endLineNumber < previewRange.endLineNumber; endLineNumber++) { + const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber); + minIndent = Math.min(minIndent, endIndent); + } + + const previewValue = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim(); + return previewValue; + } + + private getPreviewRangeBasedOnIndentation(textEditorModel: ITextModel, startLineNumber: number) { + const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber); + const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionWithMouseEditorContribution.MAX_SOURCE_PREVIEW_LINES); + let endLineNumber = startLineNumber + 1; + + for (; endLineNumber < maxLineNumber; endLineNumber++) { + let endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber); + + if (startIndent === endIndent) { + break; + } + } + + return new Range(startLineNumber, 1, endLineNumber + 1, 1); + } + + private getPreviewRangeBasedOnBrackets(textEditorModel: ITextModel, startLineNumber: number) { + const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionWithMouseEditorContribution.MAX_SOURCE_PREVIEW_LINES); + + const brackets = []; + + let ignoreFirstEmpty = true; + let currentBracket = textEditorModel.findNextBracket(new Position(startLineNumber, 1)); + while (currentBracket !== null) { + + if (brackets.length === 0) { + brackets.push(currentBracket); + } else { + const lastBracket = brackets[brackets.length - 1]; + if (lastBracket.open === currentBracket.open && lastBracket.isOpen && !currentBracket.isOpen) { + brackets.pop(); + } else { + brackets.push(currentBracket); + } + + if (brackets.length === 0) { + if (ignoreFirstEmpty) { + ignoreFirstEmpty = false; + } else { + return new Range(startLineNumber, 1, currentBracket.range.endLineNumber + 1, 1); + } + } + } + + const maxColumn = textEditorModel.getLineMaxColumn(startLineNumber); + let nextLineNumber = currentBracket.range.endLineNumber; + let nextColumn = currentBracket.range.endColumn; + if (maxColumn === currentBracket.range.endColumn) { + nextLineNumber++; + nextColumn = 1; + } + + if (nextLineNumber > maxLineNumber) { + return new Range(startLineNumber, 1, maxLineNumber + 1, 1); + } + + currentBracket = textEditorModel.findNextBracket(new Position(nextLineNumber, nextColumn)); + } + + return new Range(startLineNumber, 1, maxLineNumber + 1, 1); } private addDecoration(range: Range, hoverMessage: MarkdownString): void { @@ -194,13 +278,13 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC DefinitionProviderRegistry.has(this.editor.getModel()); } - private findDefinition(target: IMouseTarget): TPromise { - let model = this.editor.getModel(); + private findDefinition(target: IMouseTarget, token: CancellationToken): Thenable { + const model = this.editor.getModel(); if (!model) { return TPromise.as(null); } - return getDefinitionsAtPosition(this.editor.getModel(), target.position); + return getDefinitionsAtPosition(model, target.position, token); } private gotoDefinition(target: IMouseTarget, sideBySide: boolean): TPromise { diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 1c56024bc20..55c8120e4f8 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Position } from 'vs/editor/common/core/position'; @@ -19,7 +19,7 @@ import { registerEditorAction, registerEditorContribution, ServicesAccessor, IAc import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { MarkerNavigationWidget } from './gotoErrorWidget'; import { compare } from 'vs/base/common/strings'; import { binarySearch } from 'vs/base/common/arrays'; @@ -401,7 +401,8 @@ class NextMarkerInFilesAction extends MarkerNavigationAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyCode.F8 + primary: KeyCode.F8, + weight: KeybindingWeight.EditorContrib } }); } @@ -416,7 +417,8 @@ class PrevMarkerInFilesAction extends MarkerNavigationAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.Shift | KeyCode.F8 + primary: KeyMod.Shift | KeyCode.F8, + weight: KeybindingWeight.EditorContrib } }); } @@ -437,7 +439,7 @@ registerEditorCommand(new MarkerCommand({ precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE, handler: x => x.closeMarkersNavigation(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + weight: KeybindingWeight.EditorContrib + 50, kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index cfc7c049c58..ce9478eaff2 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -114,7 +114,7 @@ class MessageWidget { let relatedResource = document.createElement('span'); dom.addClass(relatedResource, 'filename'); relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; - relatedResource.title = getPathLabel(related.resource); + relatedResource.title = getPathLabel(related.resource, undefined); this._relatedDiagnostics.set(relatedResource, related); let relatedMessage = document.createElement('span'); diff --git a/src/vs/editor/contrib/hover/getHover.ts b/src/vs/editor/contrib/hover/getHover.ts index a6bf28af9c8..577c87d814a 100644 --- a/src/vs/editor/contrib/hover/getHover.ts +++ b/src/vs/editor/contrib/hover/getHover.ts @@ -3,39 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { coalesce } from 'vs/base/common/arrays'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Hover, HoverProviderRegistry } from 'vs/editor/common/modes'; -import { asWinJsPromise } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; -export function getHover(model: ITextModel, position: Position): TPromise { +export function getHover(model: ITextModel, position: Position, token: CancellationToken): Promise { const supports = HoverProviderRegistry.ordered(model); - const values: Hover[] = []; - const promises = supports.map((support, idx) => { - return asWinJsPromise((token) => { - return support.provideHover(model, position, token); - }).then((result) => { - if (result) { - let hasRange = (typeof result.range !== 'undefined'); - let hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0; - if (hasRange && hasHtmlContent) { - values[idx] = result; - } - } + const promises = supports.map(support => { + return Promise.resolve(support.provideHover(model, position, token)).then(hover => { + return hover && isValid(hover) ? hover : undefined; }, err => { onUnexpectedExternalError(err); + return undefined; }); }); - return TPromise.join(promises).then(() => coalesce(values)); + return Promise.all(promises).then(values => coalesce(values)); } -registerDefaultLanguageCommand('_executeHoverProvider', getHover); +registerDefaultLanguageCommand('_executeHoverProvider', (model, position) => getHover(model, position, CancellationToken.None)); + +function isValid(result: Hover) { + const hasRange = (typeof result.range !== 'undefined'); + const hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0; + return hasRange && hasHtmlContent; +} diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index 1fdad70ae85..655df41ddda 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -13,7 +13,8 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon'; +import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ModesContentHoverWidget } from './modesContentHover'; @@ -24,13 +25,15 @@ import { editorHoverHighlight, editorHoverBackground, editorHoverBorder, textLin import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; +import { HoverStartMode } from 'vs/editor/contrib/hover/hoverOperation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class ModesHoverController implements editorCommon.IEditorContribution { +export class ModesHoverController implements IEditorContribution { private static readonly ID = 'editor.contrib.hover'; - private _editor: ICodeEditor; private _toUnhook: IDisposable[]; + private _didChangeConfigurationHandler: IDisposable; private _contentWidget: ModesContentHoverWidget; private _glyphWidget: ModesGlyphHoverWidget; @@ -51,35 +54,57 @@ export class ModesHoverController implements editorCommon.IEditorContribution { private _isMouseDown: boolean; private _hoverClicked: boolean; + private _isHoverEnabled: boolean; + private _isHoverSticky: boolean; static get(editor: ICodeEditor): ModesHoverController { return editor.getContribution(ModesHoverController.ID); } - constructor(editor: ICodeEditor, + constructor(private readonly _editor: ICodeEditor, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, @IThemeService private readonly _themeService: IThemeService ) { - this._editor = editor; - this._toUnhook = []; - this._isMouseDown = false; - if (editor.getConfiguration().contribInfo.hover) { + this._isMouseDown = false; + this._hoverClicked = false; + + this._hookEvents(); + + this._didChangeConfigurationHandler = this._editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => { + if (e.contribInfo) { + this._hideWidgets(); + this._unhookEvents(); + this._hookEvents(); + } + }); + } + + private _hookEvents(): void { + const hideWidgetsEventHandler = () => this._hideWidgets(); + + const hoverOpts = this._editor.getConfiguration().contribInfo.hover; + this._isHoverEnabled = hoverOpts.enabled; + this._isHoverSticky = hoverOpts.sticky; + if (this._isHoverEnabled) { this._toUnhook.push(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); this._toUnhook.push(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e))); this._toUnhook.push(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); - this._toUnhook.push(this._editor.onMouseLeave((e: IEditorMouseEvent) => this._hideWidgets())); this._toUnhook.push(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e))); - this._toUnhook.push(this._editor.onDidChangeModel(() => this._hideWidgets())); this._toUnhook.push(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged())); - this._toUnhook.push(this._editor.onDidScrollChange((e) => { - if (e.scrollTopChanged || e.scrollLeftChanged) { - this._hideWidgets(); - } - })); + } else { + this._toUnhook.push(this._editor.onMouseMove(hideWidgetsEventHandler)); } + + this._toUnhook.push(this._editor.onMouseLeave(hideWidgetsEventHandler)); + this._toUnhook.push(this._editor.onDidChangeModel(hideWidgetsEventHandler)); + this._toUnhook.push(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e))); + } + + private _unhookEvents(): void { + this._toUnhook = dispose(this._toUnhook); } private _onModelDecorationsChanged(): void { @@ -87,6 +112,12 @@ export class ModesHoverController implements editorCommon.IEditorContribution { this.glyphWidget.onModelDecorationsChanged(); } + private _onEditorScrollChanged(e: IScrollEvent): void { + if (e.scrollTopChanged || e.scrollLeftChanged) { + this._hideWidgets(); + } + } + private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { this._isMouseDown = true; @@ -115,6 +146,7 @@ export class ModesHoverController implements editorCommon.IEditorContribution { } private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + // const this._editor.getConfiguration().contribInfo.hover.sticky; let targetType = mouseEvent.target.type; const hasStopKey = (platform.isMacintosh ? mouseEvent.event.metaKey : mouseEvent.event.ctrlKey); @@ -122,12 +154,12 @@ export class ModesHoverController implements editorCommon.IEditorContribution { return; } - if (targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID && !hasStopKey) { + if (this._isHoverSticky && targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID && !hasStopKey) { // mouse moved on top of content hover widget return; } - if (targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID && !hasStopKey) { + if (this._isHoverSticky && targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID && !hasStopKey) { // mouse moved on top of overlay hover widget return; } @@ -141,20 +173,26 @@ export class ModesHoverController implements editorCommon.IEditorContribution { } } - if (this._editor.getConfiguration().contribInfo.hover && targetType === MouseTargetType.CONTENT_TEXT) { + if (targetType === MouseTargetType.CONTENT_TEXT) { this.glyphWidget.hide(); - this.contentWidget.startShowingAt(mouseEvent.target.range, false); + + if (this._isHoverEnabled) { + this.contentWidget.startShowingAt(mouseEvent.target.range, HoverStartMode.Delayed, false); + } } else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) { this.contentWidget.hide(); - this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + + if (this._isHoverEnabled) { + this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + } } else { this._hideWidgets(); } } private _onKeyDown(e: IKeyboardEvent): void { - if (e.keyCode !== KeyCode.Ctrl && e.keyCode !== KeyCode.Alt && e.keyCode !== KeyCode.Meta) { - // Do not hide hover when Ctrl/Meta is pressed + if (e.keyCode !== KeyCode.Ctrl && e.keyCode !== KeyCode.Alt && e.keyCode !== KeyCode.Meta && e.keyCode !== KeyCode.Shift) { + // Do not hide hover when a modifier key is pressed this._hideWidgets(); } } @@ -174,8 +212,8 @@ export class ModesHoverController implements editorCommon.IEditorContribution { this._glyphWidget = new ModesGlyphHoverWidget(this._editor, renderer); } - public showContentHover(range: Range, focus: boolean): void { - this.contentWidget.startShowingAt(range, focus); + public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { + this.contentWidget.startShowingAt(range, mode, focus); } public getId(): string { @@ -183,7 +221,9 @@ export class ModesHoverController implements editorCommon.IEditorContribution { } public dispose(): void { - this._toUnhook = dispose(this._toUnhook); + this._unhookEvents(); + this._didChangeConfigurationHandler.dispose(); + if (this._glyphWidget) { this._glyphWidget.dispose(); this._glyphWidget = null; @@ -211,7 +251,8 @@ class ShowHoverAction extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I), + weight: KeybindingWeight.EditorContrib } }); } @@ -223,7 +264,7 @@ class ShowHoverAction extends EditorAction { } const position = editor.getPosition(); const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - controller.showContentHover(range, true); + controller.showContentHover(range, HoverStartMode.Immediate, true); } } diff --git a/src/vs/editor/contrib/hover/hoverOperation.ts b/src/vs/editor/contrib/hover/hoverOperation.ts index 432f7107116..2b55f24057d 100644 --- a/src/vs/editor/contrib/hover/hoverOperation.ts +++ b/src/vs/editor/contrib/hover/hoverOperation.ts @@ -4,21 +4,16 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IHoverComputer { - /** - * Overwrite the default hover time - */ - getHoverTimeMillis?: () => number; - /** * This is called after half the hover time */ - computeAsync?: () => TPromise; + computeAsync?: (token: CancellationToken) => Promise; /** * This is called after all the hover time @@ -46,17 +41,23 @@ const enum ComputeHoverOperationState { WAITING_FOR_ASYNC_COMPUTATION = 3 } +export const enum HoverStartMode { + Delayed = 0, + Immediate = 1 +} + export class HoverOperation { static HOVER_TIME = 300; private _computer: IHoverComputer; private _state: ComputeHoverOperationState; + private _hoverTime: number; private _firstWaitScheduler: RunOnceScheduler; private _secondWaitScheduler: RunOnceScheduler; private _loadingMessageScheduler: RunOnceScheduler; - private _asyncComputationPromise: TPromise; + private _asyncComputationPromise: CancelablePromise; private _asyncComputationPromiseDone: boolean; private _completeCallback: (r: Result) => void; @@ -66,10 +67,11 @@ export class HoverOperation { constructor(computer: IHoverComputer, success: (r: Result) => void, error: (err: any) => void, progress: (progress: any) => void) { this._computer = computer; this._state = ComputeHoverOperationState.IDLE; + this._hoverTime = HoverOperation.HOVER_TIME; - this._firstWaitScheduler = new RunOnceScheduler(() => this._triggerAsyncComputation(), this._getHoverTimeMillis() / 2); - this._secondWaitScheduler = new RunOnceScheduler(() => this._triggerSyncComputation(), this._getHoverTimeMillis() / 2); - this._loadingMessageScheduler = new RunOnceScheduler(() => this._showLoadingMessage(), 3 * this._getHoverTimeMillis()); + this._firstWaitScheduler = new RunOnceScheduler(() => this._triggerAsyncComputation(), 0); + this._secondWaitScheduler = new RunOnceScheduler(() => this._triggerSyncComputation(), 0); + this._loadingMessageScheduler = new RunOnceScheduler(() => this._showLoadingMessage(), 0); this._asyncComputationPromise = null; this._asyncComputationPromiseDone = false; @@ -79,23 +81,34 @@ export class HoverOperation { this._progressCallback = progress; } - private _getHoverTimeMillis(): number { - if (this._computer.getHoverTimeMillis) { - return this._computer.getHoverTimeMillis(); - } - return HoverOperation.HOVER_TIME; + public setHoverTime(hoverTime: number): void { + this._hoverTime = hoverTime; + } + + private _firstWaitTime(): number { + return this._hoverTime / 2; + } + + private _secondWaitTime(): number { + return this._hoverTime / 2; + } + + private _loadingMessageTime(): number { + return 3 * this._hoverTime; } private _triggerAsyncComputation(): void { this._state = ComputeHoverOperationState.SECOND_WAIT; - this._secondWaitScheduler.schedule(); + this._secondWaitScheduler.schedule(this._secondWaitTime()); if (this._computer.computeAsync) { this._asyncComputationPromiseDone = false; - this._asyncComputationPromise = this._computer.computeAsync().then((asyncResult: Result) => { + this._asyncComputationPromise = createCancelablePromise(token => this._computer.computeAsync(token)); + this._asyncComputationPromise.then((asyncResult: Result) => { this._asyncComputationPromiseDone = true; this._withAsyncResult(asyncResult); }, (e) => this._onError(e)); + } else { this._asyncComputationPromiseDone = true; } @@ -152,11 +165,25 @@ export class HoverOperation { } } - public start(): void { - if (this._state === ComputeHoverOperationState.IDLE) { - this._state = ComputeHoverOperationState.FIRST_WAIT; - this._firstWaitScheduler.schedule(); - this._loadingMessageScheduler.schedule(); + public start(mode: HoverStartMode): void { + if (mode === HoverStartMode.Delayed) { + if (this._state === ComputeHoverOperationState.IDLE) { + this._state = ComputeHoverOperationState.FIRST_WAIT; + this._firstWaitScheduler.schedule(this._firstWaitTime()); + this._loadingMessageScheduler.schedule(this._loadingMessageTime()); + } + } else { + switch (this._state) { + case ComputeHoverOperationState.IDLE: + this._triggerAsyncComputation(); + this._secondWaitScheduler.cancel(); + this._triggerSyncComputation(); + break; + case ComputeHoverOperationState.SECOND_WAIT: + this._secondWaitScheduler.cancel(); + this._triggerSyncComputation(); + break; + } } } diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index bb029b40ded..071469c7a18 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -6,13 +6,12 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { HoverProviderRegistry, Hover, IColor, DocumentColorProvider } from 'vs/editor/common/modes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { getHover } from 'vs/editor/contrib/hover/getHover'; -import { HoverOperation, IHoverComputer } from './hoverOperation'; +import { HoverOperation, IHoverComputer, HoverStartMode } from './hoverOperation'; import { ContentHoverWidget } from './hoverWidgets'; import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; @@ -21,9 +20,10 @@ import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { Color, RGBA } from 'vs/base/common/color'; -import { IDisposable, empty as EmptyDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CancellationToken } from 'vs/base/common/cancellation'; const $ = dom.$; class ColorHover { @@ -57,17 +57,17 @@ class ModesContentComputer implements IHoverComputer { this._result = []; } - computeAsync(): TPromise { + computeAsync(token: CancellationToken): Promise { const model = this._editor.getModel(); if (!HoverProviderRegistry.has(model)) { - return TPromise.as(null); + return Promise.resolve(null); } return getHover(model, new Position( this._range.startLineNumber, this._range.startColumn - )); + ), token); } computeSync(): HoverPart[] { @@ -167,8 +167,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget; - private renderDisposable: IDisposable = EmptyDisposable; - private toDispose: IDisposable[] = []; + private renderDisposable: IDisposable = Disposable.None; constructor( editor: ICodeEditor, @@ -182,7 +181,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._isChangingDecorations = false; this._markdownRenderer = markdownRenderer; - markdownRenderer.onDidRenderCodeBlock(this.onContentsChange, this, this.toDispose); + this._register(markdownRenderer.onDidRenderCodeBlock(this.onContentsChange, this)); this._hoverOperation = new HoverOperation( this._computer, @@ -191,21 +190,23 @@ export class ModesContentHoverWidget extends ContentHoverWidget { result => this._withResult(result, false) ); - this.toDispose.push(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => { + this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => { if (this._colorPicker) { dom.addClass(this.getDomNode(), 'colorpicker-hover'); } })); - this.toDispose.push(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { + this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { dom.removeClass(this.getDomNode(), 'colorpicker-hover'); })); + this._register(editor.onDidChangeConfiguration((e) => { + this._hoverOperation.setHoverTime(this._editor.getConfiguration().contribInfo.hover.delay); + })); } dispose(): void { this.renderDisposable.dispose(); - this.renderDisposable = EmptyDisposable; + this.renderDisposable = Disposable.None; this._hoverOperation.cancel(); - this.toDispose = dispose(this.toDispose); super.dispose(); } @@ -220,12 +221,12 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._computer.clearResult(); if (!this._colorPicker) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place - this._hoverOperation.start(); + this._hoverOperation.start(HoverStartMode.Delayed); } } } - startShowingAt(range: Range, focus: boolean): void { + startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { if (this._lastRange && this._lastRange.equalsRange(range)) { // We have to show the widget at the exact same range as before, so no work is needed return; @@ -262,7 +263,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._lastRange = range; this._computer.setRange(range); this._shouldFocus = focus; - this._hoverOperation.start(); + this._hoverOperation.start(mode); } hide(): void { @@ -273,7 +274,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; this.renderDisposable.dispose(); - this.renderDisposable = EmptyDisposable; + this.renderDisposable = Disposable.None; this._colorPicker = null; } @@ -338,7 +339,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { const model = new ColorPickerModel(color, [], 0); const widget = new ColorPickerWidget(fragment, model, this._editor.getConfiguration().pixelRatio, this._themeService); - getColorPresentations(editorModel, colorInfo, msg.provider).then(colorPresentations => { + getColorPresentations(editorModel, colorInfo, msg.provider, CancellationToken.None).then(colorPresentations => { model.colorPresentations = colorPresentations; const originalText = this._editor.getModel().getValueInRange(msg.range); model.guessColorPresentation(color, originalText); @@ -360,11 +361,11 @@ export class ModesContentHoverWidget extends ContentHoverWidget { newRange = range.setEndPosition(range.endLineNumber, range.startColumn + model.presentation.label.length); } - editorModel.pushEditOperations([], textEdits, () => []); + this._editor.executeEdits('colorpicker', textEdits); if (model.presentation.additionalTextEdits) { textEdits = [...model.presentation.additionalTextEdits]; - editorModel.pushEditOperations([], textEdits, () => []); + this._editor.executeEdits('colorpicker', textEdits); this.hide(); } this._editor.pushUndoStop(); @@ -380,7 +381,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { blue: color.rgba.b / 255, alpha: color.rgba.a } - }, msg.provider).then((colorPresentations) => { + }, msg.provider, CancellationToken.None).then((colorPresentations) => { model.colorPresentations = colorPresentations; }); }; diff --git a/src/vs/editor/contrib/hover/modesGlyphHover.ts b/src/vs/editor/contrib/hover/modesGlyphHover.ts index c1844e2e33e..535aab4a250 100644 --- a/src/vs/editor/contrib/hover/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/modesGlyphHover.ts @@ -5,7 +5,7 @@ 'use strict'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { HoverOperation, IHoverComputer } from './hoverOperation'; +import { HoverOperation, IHoverComputer, HoverStartMode } from './hoverOperation'; import { GlyphHoverWidget } from './hoverWidgets'; import { $ } from 'vs/base/browser/dom'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; @@ -123,7 +123,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { // we need to recompute the displayed text this._hoverOperation.cancel(); this._computer.clearResult(); - this._hoverOperation.start(); + this._hoverOperation.start(HoverStartMode.Delayed); } } @@ -139,7 +139,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { this._lastLineNumber = lineNumber; this._computer.setLineNumber(lineNumber); - this._hoverOperation.start(); + this._hoverOperation.start(HoverStartMode.Delayed); } public hide(): void { diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index 4d7b492685d..eba360f0676 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -20,6 +20,9 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; class InPlaceReplaceController implements IEditorContribution { @@ -33,11 +36,11 @@ class InPlaceReplaceController implements IEditorContribution { className: 'valueSetReplacement' }); - private editor: ICodeEditor; - private currentRequest: TPromise; - private decorationRemover: TPromise; - private decorationIds: string[]; - private editorWorkerService: IEditorWorkerService; + private readonly editor: ICodeEditor; + private readonly editorWorkerService: IEditorWorkerService; + private decorationIds: string[] = []; + private currentRequest: CancelablePromise; + private decorationRemover: CancelablePromise; constructor( editor: ICodeEditor, @@ -45,9 +48,6 @@ class InPlaceReplaceController implements IEditorContribution { ) { this.editor = editor; this.editorWorkerService = editorWorkerService; - this.currentRequest = TPromise.as(null); - this.decorationRemover = TPromise.as(null); - this.decorationIds = []; } public dispose(): void { @@ -57,10 +57,12 @@ class InPlaceReplaceController implements IEditorContribution { return InPlaceReplaceController.ID; } - public run(source: string, up: boolean): TPromise { + public run(source: string, up: boolean): Thenable { // cancel any pending request - this.currentRequest.cancel(); + if (this.currentRequest) { + this.currentRequest.cancel(); + } let selection = this.editor.getSelection(); const model = this.editor.getModel(); @@ -74,18 +76,12 @@ class InPlaceReplaceController implements IEditorContribution { const state = new EditorState(this.editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); if (!this.editorWorkerService.canNavigateValueSet(modelURI)) { - this.currentRequest = TPromise.as(null); - } else { - this.currentRequest = this.editorWorkerService.navigateValueSet(modelURI, selection, up); - this.currentRequest = this.currentRequest.then((basicResult) => { - if (basicResult && basicResult.range && basicResult.value) { - return basicResult; - } - return null; - }); + return undefined; } - return this.currentRequest.then((result: IInplaceReplaceSupportResult) => { + this.currentRequest = createCancelablePromise(token => this.editorWorkerService.navigateValueSet(modelURI, selection, up)); + + return this.currentRequest.then(result => { if (!result || !result.range || !result.value) { // No proper result @@ -127,12 +123,13 @@ class InPlaceReplaceController implements IEditorContribution { }]); // remove decoration after delay - this.decorationRemover.cancel(); - this.decorationRemover = TPromise.timeout(350); - this.decorationRemover.then(() => { - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); - }); - }); + if (this.decorationRemover) { + this.decorationRemover.cancel(); + } + this.decorationRemover = timeout(350); + this.decorationRemover.then(() => this.decorationIds = this.editor.deltaDecorations(this.decorationIds, [])).catch(onUnexpectedError); + + }).catch(onUnexpectedError); } } @@ -146,7 +143,8 @@ class InPlaceReplaceUp extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_COMMA + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_COMMA, + weight: KeybindingWeight.EditorContrib } }); } @@ -156,7 +154,7 @@ class InPlaceReplaceUp extends EditorAction { if (!controller) { return undefined; } - return controller.run(this.id, true); + return TPromise.wrap(controller.run(this.id, true)); } } @@ -170,7 +168,8 @@ class InPlaceReplaceDown extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + weight: KeybindingWeight.EditorContrib } }); } @@ -180,7 +179,7 @@ class InPlaceReplaceDown extends EditorAction { if (!controller) { return undefined; } - return controller.run(this.id, false); + return TPromise.wrap(controller.run(this.id, false)); } } diff --git a/src/vs/editor/contrib/indentation/indentation.ts b/src/vs/editor/contrib/indentation/indentation.ts index ab6c491ed74..9ba96fc6e06 100644 --- a/src/vs/editor/contrib/indentation/indentation.ts +++ b/src/vs/editor/contrib/indentation/indentation.ts @@ -5,13 +5,11 @@ import * as nls from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; import { IEditorContribution, ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, ServicesAccessor, IActionOptions, EditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -23,6 +21,7 @@ import { TextEdit, StandardTokenType } from 'vs/editor/common/modes'; import * as IndentUtil from './indentUtils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IndentConsts } from 'vs/editor/common/modes/supports/indentRules'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export function shiftIndent(tabSize: number, indentation: string, count?: number): string { count = count || 1; @@ -216,13 +215,13 @@ export class ChangeIndentationSizeAction extends EditorAction { super(opts); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): TPromise { - const quickOpenService = accessor.get(IQuickOpenService); + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const quickInputService = accessor.get(IQuickInputService); const modelService = accessor.get(IModelService); let model = editor.getModel(); if (!model) { - return undefined; + return; } let creationOpts = modelService.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); @@ -236,16 +235,16 @@ export class ChangeIndentationSizeAction extends EditorAction { // auto focus the tabSize set for the current editor const autoFocusIndex = Math.min(model.getOptions().tabSize - 1, 7); - return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).then(() => - quickOpenService.pick(picks, { placeHolder: nls.localize({ key: 'selectTabWidth', comment: ['Tab corresponds to the tab key'] }, "Select Tab Size for Current File"), autoFocus: { autoFocusIndex } }).then(pick => { + setTimeout(() => { + quickInputService.pick(picks, { placeHolder: nls.localize({ key: 'selectTabWidth', comment: ['Tab corresponds to the tab key'] }, "Select Tab Size for Current File"), activeItem: picks[autoFocusIndex] }).then(pick => { if (pick) { model.updateOptions({ tabSize: parseInt(pick.label, 10), insertSpaces: this.insertSpaces }); } - }) - ); + }); + }, 50/* quick open is sensitive to being opened so soon after another */); } } diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 0e2e13b66b6..6ec061f49ac 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -23,6 +23,8 @@ import { MoveLinesCommand } from './moveLinesCommand'; import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; // copy lines @@ -35,7 +37,7 @@ abstract class AbstractCopyLinesAction extends EditorAction { this.down = down; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { let commands: ICommand[] = []; let selections = editor.getSelections(); @@ -60,7 +62,14 @@ class CopyLinesUpAction extends AbstractCopyLinesAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow } + linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '2_line', + title: nls.localize({ key: 'miCopyLinesUp', comment: ['&& denotes a mnemonic'] }, "&&Copy Line Up"), + order: 1 } }); } @@ -76,7 +85,14 @@ class CopyLinesDownAction extends AbstractCopyLinesAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow } + linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '2_line', + title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down"), + order: 2 } }); } @@ -93,7 +109,7 @@ abstract class AbstractMoveLinesAction extends EditorAction { this.down = down; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { let commands: ICommand[] = []; let selections = editor.getSelections(); @@ -119,7 +135,14 @@ class MoveLinesUpAction extends AbstractMoveLinesAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.UpArrow, - linux: { primary: KeyMod.Alt | KeyCode.UpArrow } + linux: { primary: KeyMod.Alt | KeyCode.UpArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '2_line', + title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up"), + order: 3 } }); } @@ -135,7 +158,14 @@ class MoveLinesDownAction extends AbstractMoveLinesAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.DownArrow, - linux: { primary: KeyMod.Alt | KeyCode.DownArrow } + linux: { primary: KeyMod.Alt | KeyCode.DownArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '2_line', + title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down"), + order: 4 } }); } @@ -149,7 +179,7 @@ export abstract class AbstractSortLinesAction extends EditorAction { this.descending = descending; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { const selections = editor.getSelections(); for (let i = 0, len = selections.length; i < len; i++) { @@ -204,12 +234,13 @@ export class TrimTrailingWhitespaceAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { let cursors: Position[] = []; if (args.reason === 'auto-save') { @@ -235,8 +266,37 @@ interface IDeleteLinesOperation { positionColumn: number; } -abstract class AbstractRemoveLinesAction extends EditorAction { - _getLinesToRemove(editor: ICodeEditor): IDeleteLinesOperation[] { +class DeleteLinesAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.deleteLines', + label: nls.localize('lines.delete', "Delete Line"), + alias: 'Delete Line', + precondition: EditorContextKeys.writable, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_K, + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { + + let ops = this._getLinesToRemove(editor); + + // Finally, construct the delete lines commands + let commands: ICommand[] = ops.map((op) => { + return new DeleteLinesCommand(op.startLineNumber, op.endLineNumber, op.positionColumn); + }); + + editor.pushUndoStop(); + editor.executeCommands(this.id, commands); + editor.pushUndoStop(); + } + + private _getLinesToRemove(editor: ICodeEditor): IDeleteLinesOperation[] { // Construct delete operations let operations: IDeleteLinesOperation[] = editor.getSelections().map((s) => { @@ -277,36 +337,6 @@ abstract class AbstractRemoveLinesAction extends EditorAction { } } -class DeleteLinesAction extends AbstractRemoveLinesAction { - - constructor() { - super({ - id: 'editor.action.deleteLines', - label: nls.localize('lines.delete', "Delete Line"), - alias: 'Delete Line', - precondition: EditorContextKeys.writable, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_K - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - - let ops = this._getLinesToRemove(editor); - - // Finally, construct the delete lines commands - let commands: ICommand[] = ops.map((op) => { - return new DeleteLinesCommand(op.startLineNumber, op.endLineNumber, op.positionColumn); - }); - - editor.pushUndoStop(); - editor.executeCommands(this.id, commands); - editor.pushUndoStop(); - } -} - export class IndentLinesAction extends EditorAction { constructor() { super({ @@ -316,12 +346,13 @@ export class IndentLinesAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.indent(editor._getCursorConfiguration(), editor.getModel(), editor.getSelections())); editor.pushUndoStop(); @@ -337,12 +368,13 @@ class OutdentLinesAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); } } @@ -356,12 +388,13 @@ export class InsertLineBeforeAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.lineInsertBefore(editor._getCursorConfiguration(), editor.getModel(), editor.getSelections())); } @@ -376,19 +409,20 @@ export class InsertLineAfterAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.Enter + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.lineInsertAfter(editor._getCursorConfiguration(), editor.getModel(), editor.getSelections())); } } export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction { - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { const primaryCursor = editor.getSelection(); let rangesToDelete = this._getRangesToDelete(editor); // merge overlapping selections @@ -408,8 +442,8 @@ export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction { effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]); let endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges); + let edits: IIdentifiedSingleEditOperation[] = effectiveRanges.map(range => { - endCursorState.push(new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn)); return EditOperation.replace(range, ''); }); @@ -436,7 +470,8 @@ export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: null, - mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace } + mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, + weight: KeybindingWeight.EditorContrib } }); } @@ -444,17 +479,25 @@ export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction { _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] { let endPrimaryCursor: Selection; let endCursorState: Selection[] = []; + let deletedLines = 0; - for (let i = 0, len = rangesToDelete.length; i < len; i++) { - let range = rangesToDelete[i]; - let endCursor = new Selection(rangesToDelete[i].startLineNumber, rangesToDelete[i].startColumn, rangesToDelete[i].startLineNumber, rangesToDelete[i].startColumn); + rangesToDelete.forEach(range => { + let endCursor; + if (range.endColumn === 1 && deletedLines > 0) { + let newStartLine = range.startLineNumber - deletedLines; + endCursor = new Selection(newStartLine, range.startColumn, newStartLine, range.startColumn); + } else { + endCursor = new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); + } + + deletedLines += range.endLineNumber - range.startLineNumber; if (range.intersectRanges(primaryCursor)) { endPrimaryCursor = endCursor; } else { endCursorState.push(endCursor); } - } + }); if (endPrimaryCursor) { endCursorState.unshift(endPrimaryCursor); @@ -465,11 +508,18 @@ export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction { _getRangesToDelete(editor: ICodeEditor): Range[] { let rangesToDelete: Range[] = editor.getSelections(); + let model = editor.getModel(); rangesToDelete.sort(Range.compareRangesUsingStarts); rangesToDelete = rangesToDelete.map(selection => { if (selection.isEmpty()) { - return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn); + if (selection.startColumn === 1) { + let deleteFromLine = Math.max(1, selection.startLineNumber - 1); + let deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineContent(deleteFromLine).length + 1; + return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1); + } else { + return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn); + } } else { return selection; } @@ -489,7 +539,8 @@ export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: null, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_K, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] } + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_K, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] }, + weight: KeybindingWeight.EditorContrib } }); } @@ -546,12 +597,13 @@ export class JoinLinesAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_J } + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_J }, + weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { let selections = editor.getSelections(); let primaryCursor = editor.getSelection(); @@ -694,7 +746,7 @@ export class TransposeAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { let selections = editor.getSelections(); let model = editor.getModel(); let commands: ICommand[] = []; @@ -735,7 +787,7 @@ export class TransposeAction extends EditorAction { } export abstract class AbstractCaseAction extends EditorAction { - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { let selections = editor.getSelections(); let model = editor.getModel(); let commands: ICommand[] = []; diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index ae01a2682eb..31963d45986 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -155,6 +155,101 @@ suite('Editor Contrib - Line Operations', () => { }); }); + test('should jump to the previous line when on first column', function () { + withTestCodeEditor( + [ + 'one', + 'two', + 'three' + ], {}, (editor, cursor) => { + let model = editor.getModel(); + let deleteAllLeftAction = new DeleteAllLeftAction(); + + editor.setSelection(new Selection(2, 1, 2, 1)); + deleteAllLeftAction.run(null, editor); + assert.equal(model.getLineContent(1), 'onetwo', '001'); + + editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); + deleteAllLeftAction.run(null, editor); + assert.equal(model.getLinesContent()[0], 'onetwothree'); + assert.equal(model.getLinesContent().length, 1); + + editor.setSelection(new Selection(1, 1, 1, 1)); + deleteAllLeftAction.run(null, editor); + assert.equal(model.getLinesContent()[0], 'onetwothree'); + }); + }); + + test('should keep deleting lines in multi cursor mode', function () { + withTestCodeEditor( + [ + 'hi my name is Carlos Matos', + 'BCC', + 'waso waso waso', + 'my wife doesnt believe in me', + 'nonononono', + 'bitconneeeect' + ], {}, (editor, cursor) => { + let model = editor.getModel(); + let deleteAllLeftAction = new DeleteAllLeftAction(); + + const beforeSecondWasoSelection = new Selection(3, 5, 3, 5); + const endOfBCCSelection = new Selection(2, 4, 2, 4); + const endOfNonono = new Selection(5, 11, 5, 11); + + editor.setSelections([beforeSecondWasoSelection, endOfBCCSelection, endOfNonono]); + let selections; + + deleteAllLeftAction.run(null, editor); + selections = editor.getSelections(); + + assert.equal(model.getLineContent(2), ''); + assert.equal(model.getLineContent(3), ' waso waso'); + assert.equal(model.getLineContent(5), ''); + + assert.deepEqual([ + selections[0].startLineNumber, + selections[0].startColumn, + selections[0].endLineNumber, + selections[0].endColumn + ], [3, 1, 3, 1]); + + assert.deepEqual([ + selections[1].startLineNumber, + selections[1].startColumn, + selections[1].endLineNumber, + selections[1].endColumn + ], [2, 1, 2, 1]); + + assert.deepEqual([ + selections[2].startLineNumber, + selections[2].startColumn, + selections[2].endLineNumber, + selections[2].endColumn + ], [5, 1, 5, 1]); + + deleteAllLeftAction.run(null, editor); + selections = editor.getSelections(); + + assert.equal(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); + assert.equal(selections.length, 2); + + assert.deepEqual([ + selections[0].startLineNumber, + selections[0].startColumn, + selections[0].endLineNumber, + selections[0].endColumn + ], [1, 27, 1, 27]); + + assert.deepEqual([ + selections[1].startLineNumber, + selections[1].startColumn, + selections[1].endLineNumber, + selections[1].endColumn + ], [2, 29, 2, 29]); + }); + }); + test('should work in multi cursor mode', function () { withTestCodeEditor( [ diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index bbeda1aedd4..028177b91c5 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -6,14 +6,14 @@ 'use strict'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { ILink, LinkProvider, LinkProviderRegistry } from 'vs/editor/common/modes'; -import { asWinJsPromise } from 'vs/base/common/async'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class Link implements ILink { @@ -40,7 +40,7 @@ export class Link implements ILink { return this._link.url; } - resolve(): TPromise { + resolve(token: CancellationToken): Thenable { if (this._link.url) { try { return TPromise.as(URI.parse(this._link.url)); @@ -50,11 +50,11 @@ export class Link implements ILink { } if (typeof this._provider.resolveLink === 'function') { - return asWinJsPromise(token => this._provider.resolveLink(this._link, token)).then(value => { + return Promise.resolve(this._provider.resolveLink(this._link, token)).then(value => { this._link = value || this._link; if (this._link.url) { // recurse - return this.resolve(); + return this.resolve(token); } return TPromise.wrapError(new Error('missing')); @@ -65,13 +65,13 @@ export class Link implements ILink { } } -export function getLinks(model: ITextModel): TPromise { +export function getLinks(model: ITextModel, token: CancellationToken): Promise { let links: Link[] = []; // ask all providers for links in parallel const promises = LinkProviderRegistry.ordered(model).reverse().map(provider => { - return asWinJsPromise(token => provider.provideLinks(model, token)).then(result => { + return Promise.resolve(provider.provideLinks(model, token)).then(result => { if (Array.isArray(result)) { const newLinks = result.map(link => new Link(link, provider)); links = union(links, newLinks); @@ -79,7 +79,7 @@ export function getLinks(model: ITextModel): TPromise { }, onUnexpectedExternalError); }); - return TPromise.join(promises).then(() => { + return Promise.all(promises).then(() => { return links; }); } @@ -137,5 +137,5 @@ CommandsRegistry.registerCommand('_executeLinkProvider', (accessor, ...args) => return undefined; } - return getLinks(model); + return getLinks(model, CancellationToken.None); }); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index cea850c2a20..eb65d4e2661 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -9,7 +9,6 @@ import 'vs/css!./links'; import * as nls from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as platform from 'vs/base/common/platform'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; @@ -25,6 +24,8 @@ import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'v import { MarkdownString } from 'vs/base/common/htmlContent'; import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import * as async from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; const HOVER_MESSAGE_GENERAL_META = new MarkdownString().appendText( platform.isMacintosh @@ -149,8 +150,8 @@ class LinkDetector implements editorCommon.IEditorContribution { private editor: ICodeEditor; private enabled: boolean; private listenersToRemove: IDisposable[]; - private timeoutPromise: TPromise; - private computePromise: TPromise; + private timeout: async.TimeoutTimer; + private computePromise: async.CancelablePromise; private activeLinkDecorationId: string; private openerService: IOpenerService; private notificationService: INotificationService; @@ -201,7 +202,7 @@ class LinkDetector implements editorCommon.IEditorContribution { this.listenersToRemove.push(editor.onDidChangeModelLanguage((e) => this.onModelModeChanged())); this.listenersToRemove.push(LinkProviderRegistry.onDidChange((e) => this.onModelModeChanged())); - this.timeoutPromise = null; + this.timeout = new async.TimeoutTimer(); this.computePromise = null; this.currentOccurrences = {}; this.activeLinkDecorationId = null; @@ -225,16 +226,10 @@ class LinkDetector implements editorCommon.IEditorContribution { } private onChange(): void { - if (!this.timeoutPromise) { - this.timeoutPromise = TPromise.timeout(LinkDetector.RECOMPUTE_TIME); - this.timeoutPromise.then(() => { - this.timeoutPromise = null; - this.beginCompute(); - }); - } + this.timeout.setIfNotSet(() => this.beginCompute(), LinkDetector.RECOMPUTE_TIME); } - private beginCompute(): void { + private async beginCompute(): Promise { if (!this.editor.getModel() || !this.enabled) { return; } @@ -243,10 +238,15 @@ class LinkDetector implements editorCommon.IEditorContribution { return; } - this.computePromise = getLinks(this.editor.getModel()).then(links => { + this.computePromise = async.createCancelablePromise(token => getLinks(this.editor.getModel(), token)); + try { + const links = await this.computePromise; this.updateDecorations(links); + } catch (err) { + onUnexpectedError(err); + } finally { this.computePromise = null; - }); + } } private updateDecorations(links: Link[]): void { @@ -326,7 +326,7 @@ class LinkDetector implements editorCommon.IEditorContribution { const { link } = occurrence; - link.resolve().then(uri => { + link.resolve(CancellationToken.None).then(uri => { // open the uri return this.openerService.open(uri, { openToSide }); @@ -339,7 +339,7 @@ class LinkDetector implements editorCommon.IEditorContribution { } else { onUnexpectedError(err); } - }).done(null, onUnexpectedError); + }); } public getLinkOccurrence(position: Position): LinkOccurrence { @@ -369,10 +369,7 @@ class LinkDetector implements editorCommon.IEditorContribution { } private stop(): void { - if (this.timeoutPromise) { - this.timeoutPromise.cancel(); - this.timeoutPromise = null; - } + this.timeout.cancel(); if (this.computePromise) { this.computePromise.cancel(); this.computePromise = null; @@ -382,6 +379,7 @@ class LinkDetector implements editorCommon.IEditorContribution { public dispose(): void { this.listenersToRemove = dispose(this.listenersToRemove); this.stop(); + this.timeout.dispose(); } } diff --git a/src/vs/editor/contrib/markdown/markdownRenderer.ts b/src/vs/editor/contrib/markdown/markdownRenderer.ts index 106f60d7451..6f7d78e7db4 100644 --- a/src/vs/editor/contrib/markdown/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdown/markdownRenderer.ts @@ -10,7 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown, RenderOptions } from 'vs/base/browser/htmlContentRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index e80d0f6b90c..ad00fce472f 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -7,7 +7,7 @@ import 'vs/css!./messageController'; import * as nls from 'vs/nls'; -import { setDisposableTimeout } from 'vs/base/common/async'; +import { TimeoutTimer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { alert } from 'vs/base/browser/ui/aria/aria'; @@ -18,8 +18,8 @@ import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPosit import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IPosition } from 'vs/editor/common/core/position'; import { registerThemingParticipant, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; -import { inputValidationInfoBorder, inputValidationInfoBackground } from 'vs/platform/theme/common/colorRegistry'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground } from 'vs/platform/theme/common/colorRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class MessageController extends Disposable implements editorCommon.IEditorContribution { @@ -75,7 +75,7 @@ export class MessageController extends Disposable implements editorCommon.IEdito this._messageListeners.push(this._editor.onDidChangeModel(() => this.closeMessage())); // close after 3s - this._messageListeners.push(setDisposableTimeout(() => this.closeMessage(), 3000)); + this._messageListeners.push(new TimeoutTimer(() => this.closeMessage(), 3000)); // close on mouse move let bounds: Range; @@ -114,7 +114,7 @@ registerEditorCommand(new MessageCommand({ precondition: MessageController.MESSAGE_VISIBLE, handler: c => c.closeMessage(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(30), + weight: KeybindingWeight.EditorContrib + 30, primary: KeyCode.Escape } })); @@ -194,4 +194,8 @@ registerThemingParticipant((theme, collector) => { if (background) { collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { background-color: ${background}; }`); } + let foreground = theme.getColor(inputValidationInfoForeground); + if (foreground) { + collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { color: ${foreground}; }`); + } }); diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index c47b0080fbf..f75b50f5313 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -25,6 +25,8 @@ import { overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/com import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { INewFindReplaceState, FindOptionOverride } from 'vs/editor/contrib/find/findState'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; export class InsertCursorAbove extends EditorAction { @@ -40,7 +42,14 @@ export class InsertCursorAbove extends EditorAction { linux: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow] - } + }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miInsertCursorAbove', comment: ['&& denotes a mnemonic'] }, "&&Add Cursor Above"), + order: 2 } }); } @@ -78,7 +87,14 @@ export class InsertCursorBelow extends EditorAction { linux: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow] - } + }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miInsertCursorBelow', comment: ['&& denotes a mnemonic'] }, "A&&dd Cursor Below"), + order: 3 } }); } @@ -112,7 +128,14 @@ class InsertCursorAtEndOfEachLineSelected extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_I + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_I, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miInsertCursorAtEndOfEachLineSelected', comment: ['&& denotes a mnemonic'] }, "Add C&&ursors to Line Ends"), + order: 4 } }); } @@ -522,7 +545,14 @@ export class AddSelectionToNextFindMatchAction extends MultiCursorSelectionContr precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_D + primary: KeyMod.CtrlCmd | KeyCode.KEY_D, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miAddSelectionToNextFindMatch', comment: ['&& denotes a mnemonic'] }, "Add &&Next Occurrence"), + order: 5 } }); } @@ -537,7 +567,13 @@ export class AddSelectionToPreviousFindMatchAction extends MultiCursorSelectionC id: 'editor.action.addSelectionToPreviousFindMatch', label: nls.localize('addSelectionToPreviousFindMatch', "Add Selection To Previous Find Match"), alias: 'Add Selection To Previous Find Match', - precondition: null + precondition: null, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miAddSelectionToPreviousFindMatch', comment: ['&& denotes a mnemonic'] }, "Add P&&revious Occurrence"), + order: 6 + } }); } protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { @@ -554,7 +590,8 @@ export class MoveSelectionToNextFindMatchAction extends MultiCursorSelectionCont precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D), + weight: KeybindingWeight.EditorContrib } }); } @@ -586,7 +623,14 @@ export class SelectHighlightsAction extends MultiCursorSelectionControllerAction precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '3_multi', + title: nls.localize({ key: 'miSelectHighlights', comment: ['&& denotes a mnemonic'] }, "Select All &&Occurrences"), + order: 7 } }); } @@ -604,7 +648,8 @@ export class CompatChangeAll extends MultiCursorSelectionControllerAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.F2 + primary: KeyMod.CtrlCmd | KeyCode.F2, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: '1_modification', @@ -618,13 +663,11 @@ export class CompatChangeAll extends MultiCursorSelectionControllerAction { } class SelectionHighlighterState { - public readonly lastWordUnderCursor: Selection; public readonly searchText: string; public readonly matchCase: boolean; public readonly wordSeparators: string; - constructor(lastWordUnderCursor: Selection, searchText: string, matchCase: boolean, wordSeparators: string) { - this.lastWordUnderCursor = lastWordUnderCursor; + constructor(searchText: string, matchCase: boolean, wordSeparators: string) { this.searchText = searchText; this.matchCase = matchCase; this.wordSeparators = wordSeparators; @@ -678,7 +721,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut if (e.selection.isEmpty()) { if (e.reason === CursorChangeReason.Explicit) { - if (this.state && (!this.state.lastWordUnderCursor || !this.state.lastWordUnderCursor.containsPosition(e.selection.getStartPosition()))) { + if (this.state) { // no longer valid this._setState(null); } @@ -746,21 +789,10 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut return null; } - let lastWordUnderCursor: Selection = null; - const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model); if (r.currentMatch) { // This is an empty selection - if (hasFindOccurrences) { - // Do not interfere with semantic word highlighting in the no selection case - return null; - } - - const config = editor.getConfiguration(); - if (!config.contribInfo.occurrencesHighlight) { - return null; - } - - lastWordUnderCursor = r.currentMatch; + // Do not interfere with semantic word highlighting in the no selection case + return null; } if (/^[ \t]+$/.test(r.searchText)) { // whitespace only selection @@ -792,7 +824,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut } } - return new SelectionHighlighterState(lastWordUnderCursor, r.searchText, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null); + return new SelectionHighlighterState(r.searchText, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null); } private _setState(state: SelectionHighlighterState): void { @@ -834,7 +866,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut const cmp = Range.compareRangesUsingStarts(match, selections[j]); if (cmp < 0) { // match is before sel - if (!Range.areIntersecting(match, selections[j])) { + if (selections[j].isEmpty() || !Range.areIntersecting(match, selections[j])) { matches.push(match); } i++; diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.ts b/src/vs/editor/contrib/parameterHints/parameterHints.ts index 92d7eb7a799..e02faeb4cc5 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHints.ts @@ -15,7 +15,8 @@ import { registerEditorAction, registerEditorContribution, ServicesAccessor, Edi import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ParameterHintsWidget } from './parameterHintsWidget'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import * as modes from 'vs/editor/common/modes'; class ParameterHintsController implements IEditorContribution { @@ -49,8 +50,8 @@ class ParameterHintsController implements IEditorContribution { this.widget.next(); } - trigger(): void { - this.widget.trigger(); + trigger(context: modes.SignatureHelpContext): void { + this.widget.trigger(context); } dispose(): void { @@ -68,7 +69,8 @@ export class TriggerParameterHintsAction extends EditorAction { precondition: EditorContextKeys.hasSignatureHelpProvider, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Space + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Space, + weight: KeybindingWeight.EditorContrib } }); } @@ -76,7 +78,7 @@ export class TriggerParameterHintsAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { let controller = ParameterHintsController.get(editor); if (controller) { - controller.trigger(); + controller.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }); } } } @@ -84,7 +86,7 @@ export class TriggerParameterHintsAction extends EditorAction { registerEditorContribution(ParameterHintsController); registerEditorAction(TriggerParameterHintsAction); -const weight = KeybindingsRegistry.WEIGHT.editorContrib(75); +const weight = KeybindingWeight.EditorContrib + 75; const ParameterHintsCommand = EditorCommand.bindToContribution(ParameterHintsController.get); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index d6dd252f9f9..892eb88a8e9 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -8,12 +8,11 @@ import 'vs/css!./parameterHints'; import * as nls from 'vs/nls'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { SignatureHelp, SignatureInformation, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event, Emitter, chain } from 'vs/base/common/event'; import { domEvent, stop } from 'vs/base/browser/event'; @@ -32,43 +31,49 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; const $ = dom.$; export interface IHintEvent { - hints: SignatureHelp; + hints: modes.SignatureHelp; } export class ParameterHintsModel extends Disposable { - static DELAY = 120; // ms + private static readonly DEFAULT_DELAY = 120; // ms - private _onHint = this._register(new Emitter()); - onHint: Event = this._onHint.event; + private readonly _onHint = this._register(new Emitter()); + public readonly onHint: Event = this._onHint.event; - private _onCancel = this._register(new Emitter()); - onCancel: Event = this._onCancel.event; + private readonly _onCancel = this._register(new Emitter()); + public readonly onCancel: Event = this._onCancel.event; private editor: ICodeEditor; private enabled: boolean; private triggerCharactersListeners: IDisposable[]; - private active: boolean; - private throttledDelayer: RunOnceScheduler; - private provideSignatureHelpRequest?: TPromise; + private active: boolean = false; + private pending: boolean = false; + private triggerChars = new CharacterSet(); - constructor(editor: ICodeEditor) { + private triggerContext: modes.SignatureHelpContext | undefined; + private throttledDelayer: RunOnceScheduler; + private provideSignatureHelpRequest?: CancelablePromise; + + constructor( + editor: ICodeEditor, + delay: number = ParameterHintsModel.DEFAULT_DELAY + ) { super(); this.editor = editor; this.enabled = false; this.triggerCharactersListeners = []; - this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), ParameterHintsModel.DELAY); - - this.active = false; + this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), delay); this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange())); this._register(this.editor.onDidChangeModel(e => this.onModelChanged())); this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged())); this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e))); this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange())); - this._register(SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this)); + this._register(modes.SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this)); + this._register(this.editor.onDidType(text => this.onDidType(text))); this.onEditorConfigurationChange(); this.onModelChanged(); @@ -76,6 +81,8 @@ export class ParameterHintsModel extends Disposable { cancel(silent: boolean = false): void { this.active = false; + this.pending = false; + this.triggerContext = undefined; this.throttledDelayer.cancel(); @@ -89,12 +96,13 @@ export class ParameterHintsModel extends Disposable { } } - trigger(delay = ParameterHintsModel.DELAY): void { - if (!SignatureHelpProviderRegistry.has(this.editor.getModel())) { + trigger(context: modes.SignatureHelpContext, delay?: number): void { + if (!modes.SignatureHelpProviderRegistry.has(this.editor.getModel())) { return; } this.cancel(true); + this.triggerContext = context; return this.throttledDelayer.schedule(delay); } @@ -103,73 +111,90 @@ export class ParameterHintsModel extends Disposable { this.provideSignatureHelpRequest.cancel(); } - this.provideSignatureHelpRequest = provideSignatureHelp(this.editor.getModel(), this.editor.getPosition()) - .then(null, onUnexpectedError) - .then(result => { - if (!result || !result.signatures || result.signatures.length === 0) { - this.cancel(); - this._onCancel.fire(void 0); - return false; - } + this.pending = true; - this.active = true; + const triggerContext = this.triggerContext || { triggerReason: modes.SignatureHelpTriggerReason.Invoke }; + this.triggerContext = undefined; - const event: IHintEvent = { hints: result }; - this._onHint.fire(event); - return true; - }); + this.provideSignatureHelpRequest = createCancelablePromise(token => + provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), triggerContext, token)); + + this.provideSignatureHelpRequest.then(result => { + this.pending = false; + + if (!result || !result.signatures || result.signatures.length === 0) { + this.cancel(); + this._onCancel.fire(void 0); + return false; + } + + this.active = true; + const event: IHintEvent = { hints: result }; + this._onHint.fire(event); + return true; + + }).catch(error => { + this.pending = false; + onUnexpectedError(error); + }); } - isTriggered(): boolean { - return this.active || this.throttledDelayer.isScheduled(); + private get isTriggered(): boolean { + return this.active || this.pending || this.throttledDelayer.isScheduled(); } private onModelChanged(): void { this.cancel(); - this.triggerCharactersListeners = dispose(this.triggerCharactersListeners); + // Update trigger characters + this.triggerChars = new CharacterSet(); const model = this.editor.getModel(); if (!model) { return; } - const triggerChars = new CharacterSet(); - for (const support of SignatureHelpProviderRegistry.ordered(model)) { + for (const support of modes.SignatureHelpProviderRegistry.ordered(model)) { if (Array.isArray(support.signatureHelpTriggerCharacters)) { for (const ch of support.signatureHelpTriggerCharacters) { - triggerChars.add(ch.charCodeAt(0)); + this.triggerChars.add(ch.charCodeAt(0)); } } } + } - this.triggerCharactersListeners.push(this.editor.onDidType((text: string) => { - if (!this.enabled) { - return; - } + private onDidType(text: string) { + if (!this.enabled) { + return; + } - if (triggerChars.has(text.charCodeAt(text.length - 1))) { - this.trigger(); - } - })); + const lastCharIndex = text.length - 1; + if (this.triggerChars.has(text.charCodeAt(lastCharIndex))) { + this.trigger({ + triggerReason: this.isTriggered + ? modes.SignatureHelpTriggerReason.Retrigger + : modes.SignatureHelpTriggerReason.TriggerCharacter, + triggerCharacter: text.charAt(lastCharIndex) + }); + } } private onCursorChange(e: ICursorSelectionChangedEvent): void { if (e.source === 'mouse') { this.cancel(); - } else if (this.isTriggered()) { - this.trigger(); + } else if (this.isTriggered) { + this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger }); } } private onModelContentChange(): void { - if (this.isTriggered()) { - this.trigger(); + if (this.isTriggered) { + this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger }); } } private onEditorConfigurationChange(): void { - this.enabled = this.editor.getConfiguration().contribInfo.parameterHints; + this.enabled = this.editor.getConfiguration().contribInfo.parameterHints.enabled; if (!this.enabled) { this.cancel(); @@ -188,18 +213,18 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { private static readonly ID = 'editor.widget.parameterHintsWidget'; - private markdownRenderer: MarkdownRenderer; + private readonly markdownRenderer: MarkdownRenderer; private renderDisposeables: IDisposable[]; private model: ParameterHintsModel; - private keyVisible: IContextKey; - private keyMultipleSignatures: IContextKey; + private readonly keyVisible: IContextKey; + private readonly keyMultipleSignatures: IContextKey; private element: HTMLElement; private signature: HTMLElement; private docs: HTMLElement; private overloads: HTMLElement; private currentSignature: number; private visible: boolean; - private hints: SignatureHelp; + private hints: modes.SignatureHelp; private announcedLabel: string; private scrollbar: DomScrollableElement; private disposables: IDisposable[]; @@ -294,7 +319,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { this.keyVisible.set(true); this.visible = true; - TPromise.timeout(100).done(() => dom.addClass(this.element, 'visible')); + setTimeout(() => dom.addClass(this.element, 'visible'), 100); this.editor.layoutContentWidget(this); } @@ -406,7 +431,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { this.scrollbar.scanDomNode(); } - private renderParameters(parent: HTMLElement, signature: SignatureInformation, currentParameter: number): void { + private renderParameters(parent: HTMLElement, signature: modes.SignatureInformation, currentParameter: number): void { let end = signature.label.length; let idx = 0; let element: HTMLSpanElement; @@ -475,26 +500,42 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { next(): boolean { const length = this.hints.signatures.length; + const last = (this.currentSignature % length) === (length - 1); + const cycle = this.editor.getConfiguration().contribInfo.parameterHints.cycle; - if (length < 2) { + // If there is only one signature, or we're on last signature of list + if ((length < 2 || last) && !cycle) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature + 1) % length; + if (last && cycle) { + this.currentSignature = 0; + } else { + this.currentSignature++; + } + this.render(); return true; } previous(): boolean { const length = this.hints.signatures.length; + const first = this.currentSignature === 0; + const cycle = this.editor.getConfiguration().contribInfo.parameterHints.cycle; - if (length < 2) { + // If there is only one signature, or we're on first signature of list + if ((length < 2 || first) && !cycle) { this.cancel(); return false; } - this.currentSignature = (this.currentSignature - 1 + length) % length; + if (first && cycle) { + this.currentSignature = length - 1; + } else { + this.currentSignature--; + } + this.render(); return true; } @@ -511,8 +552,8 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { return ParameterHintsWidget.ID; } - trigger(): void { - this.model.trigger(0); + trigger(context: modes.SignatureHelpContext): void { + this.model.trigger(context, 0); } private updateMaxHeight(): void { diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index 29fe15c2ae2..c75a78f58f9 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,39 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; +import { first2 } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; -import { SignatureHelp, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; -import { asWinJsPromise, sequence } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import * as modes from 'vs/editor/common/modes'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const Context = { Visible: new RawContextKey('parameterHintsVisible', false), MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp(model: ITextModel, position: Position): TPromise { +export function provideSignatureHelp(model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken): Promise { - const supports = SignatureHelpProviderRegistry.ordered(model); - let result: SignatureHelp; + const supports = modes.SignatureHelpProviderRegistry.ordered(model); - return sequence(supports.map(support => () => { - - if (result) { - // stop when there is a result - return undefined; - } - - return asWinJsPromise(token => support.provideSignatureHelp(model, position, token)).then(thisResult => { - result = thisResult; - }, onUnexpectedExternalError); - - })).then(() => result); + return first2(supports.map(support => () => { + return Promise.resolve(support.provideSignatureHelp(model, position, token, context)).catch(onUnexpectedExternalError); + })); } -registerDefaultLanguageCommand('_executeSignatureHelpProvider', provideSignatureHelp); +registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => + provideSignatureHelp(model, position, { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None)); diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts index 566c400dad3..edd554911cf 100644 --- a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -5,60 +5,214 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; +import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { SignatureHelp, SignatureHelpProvider, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; -import { TestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import * as modes from 'vs/editor/common/modes'; +import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IStorageService, NullStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { ParameterHintsModel } from '../parameterHintsWidget'; -function createMockEditor(model: TextModel): TestCodeEditor { - return createTestCodeEditor({ - model: model, - serviceCollection: new ServiceCollection( - [ITelemetryService, NullTelemetryService], - [IStorageService, NullStorageService] - ) - }); -} +const mockFile = URI.parse('test:somefile.ttt'); +const mockFileSelector = { scheme: 'test' }; +const emptySigHelpResult = { + signatures: [{ + label: 'none', + parameters: [] + }], + activeParameter: 0, + activeSignature: 0 +}; suite('ParameterHintsModel', () => { let disposables: IDisposable[] = []; - setup(function () { disposables = dispose(disposables); }); - test('Should cancel existing request when new request comes in', () => { - const textModel = TextModel.createFromString('abc def', undefined, undefined, URI.parse('test:somefile.ttt')); + function createMockEditor(fileContents: string) { + const textModel = TextModel.createFromString(fileContents, undefined, undefined, mockFile); + const editor = createTestCodeEditor({ + model: textModel, + serviceCollection: new ServiceCollection( + [ITelemetryService, NullTelemetryService], + [IStorageService, NullStorageService] + ) + }); disposables.push(textModel); + disposables.push(editor); + return editor; + } - const editor = createMockEditor(textModel); + test('Provider should get trigger character on type', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor)); + + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + return undefined; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should be retriggered if already active', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + // Retrigger + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + } else { + assert.strictEqual(invokeCount, 2); + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + } + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should not be retriggered if previous help is canceled first', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + const hintModel = new ParameterHintsModel(editor); + disposables.push(hintModel); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + + // Cancel and retrigger + hintModel.cancel(); + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + } else { + assert.strictEqual(invokeCount, 2); + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + } + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should get last trigger character when triggered multiple times and only be invoked once', (done) => { + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor, 5)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = ['a', 'b', 'c']; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, 'c'); + + // Give some time to allow for later triggers + setTimeout(() => { + assert.strictEqual(invokeCount, 1); + + done(); + }, 50); + return undefined; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: 'a' }); + editor.trigger('keyboard', Handler.Type, { text: 'b' }); + editor.trigger('keyboard', Handler.Type, { text: 'c' }); + }); + + test('Provider should be retriggered if already active', (done) => { + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor, 5)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = ['a', 'b']; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, 'a'); + + // retrigger after delay for widget to show up + setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: 'b' }), 50); + } else if (invokeCount === 2) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, 'b'); + done(); + } else { + assert.fail('Unexpected invoke'); + } + + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: 'a' }); + }); + + test('Should cancel existing request when new request comes in', () => { + const editor = createMockEditor('abc def'); const hintsModel = new ParameterHintsModel(editor); let didRequestCancellationOf = -1; let invokeCount = 0; - const longRunningProvider = new class implements SignatureHelpProvider { - signatureHelpTriggerCharacters: string[] = []; + const longRunningProvider = new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = []; - provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable { + provideSignatureHelp(_model: ITextModel, _position: Position, token: CancellationToken): modes.SignatureHelp | Thenable { const count = invokeCount++; token.onCancellationRequested(() => { didRequestCancellationOf = count; }); // retrigger on first request if (count === 0) { - hintsModel.trigger(0); + hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0); } - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { resolve({ signatures: [{ @@ -73,9 +227,9 @@ suite('ParameterHintsModel', () => { } }; - disposables.push(SignatureHelpProviderRegistry.register({ scheme: 'test' }, longRunningProvider)); + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, longRunningProvider)); - hintsModel.trigger(0); + hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0); assert.strictEqual(-1, didRequestCancellationOf); return new Promise((resolve, reject) => diff --git a/src/vs/editor/contrib/quickOpen/quickOpen.ts b/src/vs/editor/contrib/quickOpen/quickOpen.ts index 05c2680d57c..0f863de7d88 100644 --- a/src/vs/editor/contrib/quickOpen/quickOpen.ts +++ b/src/vs/editor/contrib/quickOpen/quickOpen.ts @@ -6,22 +6,21 @@ 'use strict'; import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; -import { SymbolInformation, DocumentSymbolProviderRegistry, IOutline } from 'vs/editor/common/modes'; +import { DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; -export function getDocumentSymbols(model: ITextModel): TPromise { +export function getDocumentSymbols(model: ITextModel, flat: boolean, token: CancellationToken): Thenable { - let roots: SymbolInformation[] = []; + let roots: DocumentSymbol[] = []; let promises = DocumentSymbolProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideDocumentSymbols(model, token)).then(result => { + return Promise.resolve(support.provideDocumentSymbols(model, token)).then(result => { if (Array.isArray(result)) { roots.push(...result); } @@ -30,30 +29,34 @@ export function getDocumentSymbols(model: ITextModel): TPromise { }); }); - return TPromise.join(promises).then(() => { - let flatEntries: SymbolInformation[] = []; - flatten(flatEntries, roots, ''); + return Promise.all(promises).then(() => { + let flatEntries: DocumentSymbol[] = []; + if (token.isCancellationRequested) { + return flatEntries; + } + if (flat) { + flatten(flatEntries, roots, ''); + } else { + flatEntries = roots; + } flatEntries.sort(compareEntriesUsingStart); - - return { - entries: flatEntries, - }; + return flatEntries; }); } -function compareEntriesUsingStart(a: SymbolInformation, b: SymbolInformation): number { - return Range.compareRangesUsingStarts(a.location.range, b.location.range); +function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { + return Range.compareRangesUsingStarts(a.range, b.range); } -function flatten(bucket: SymbolInformation[], entries: SymbolInformation[], overrideContainerLabel: string): void { +function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { for (let entry of entries) { bucket.push({ kind: entry.kind, name: entry.name, detail: entry.detail, containerName: entry.containerName || overrideContainerLabel, - location: entry.location, - definingRange: entry.definingRange, + range: entry.range, + selectionRange: entry.selectionRange, children: undefined, // we flatten it... }); if (entry.children) { @@ -72,5 +75,5 @@ registerLanguageCommand('_executeDocumentSymbolProvider', function (accessor, ar if (!model) { throw illegalArgument('resource'); } - return getDocumentSymbols(model); + return getDocumentSymbols(model, false, CancellationToken.None); }); diff --git a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts index 90e61d8c44c..12d77231487 100644 --- a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts @@ -5,22 +5,22 @@ 'use strict'; -import 'vs/css!./media/peekViewWidget'; -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import * as strings from 'vs/base/common/strings'; -import * as objects from 'vs/base/common/objects'; -import { $ } from 'vs/base/browser/builder'; -import { Event, Emitter } from 'vs/base/common/event'; import * as dom from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionBar, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IOptions, ZoneWidget, IStyles } from 'vs/editor/contrib/zoneWidget/zoneWidget'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; +import * as objects from 'vs/base/common/objects'; +import * as strings from 'vs/base/common/strings'; +import 'vs/css!./media/peekViewWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export namespace PeekContext { export const inPeekEditor = new RawContextKey('inReferenceSearchEditor', true); @@ -111,8 +111,8 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _fillContainer(container: HTMLElement): void { this.setCssClass('peekview-widget'); - this._headElement = $('.head').getHTMLElement(); - this._bodyElement = $('.body').getHTMLElement(); + this._headElement = dom.$('.head'); + this._bodyElement = dom.$('.body'); this._fillHead(this._headElement); this._fillBody(this._bodyElement); @@ -122,18 +122,20 @@ export abstract class PeekViewWidget extends ZoneWidget { } protected _fillHead(container: HTMLElement): void { - const titleElement = $('.peekview-title'). - on(dom.EventType.CLICK, e => this._onTitleClick(e)). - appendTo(this._headElement). - getHTMLElement(); + const titleElement = dom.$('.peekview-title'); + dom.append(this._headElement, titleElement); + dom.addStandardDisposableListener(titleElement, 'click', event => this._onTitleClick(event)); - this._primaryHeading = $('span.filename').appendTo(titleElement).getHTMLElement(); - this._secondaryHeading = $('span.dirname').appendTo(titleElement).getHTMLElement(); - this._metaHeading = $('span.meta').appendTo(titleElement).getHTMLElement(); + this._primaryHeading = dom.$('span.filename'); + this._secondaryHeading = dom.$('span.dirname'); + this._metaHeading = dom.$('span.meta'); + dom.append(titleElement, this._primaryHeading, this._secondaryHeading, this._metaHeading); + + const actionsContainer = dom.$('.peekview-actions'); + dom.append(this._headElement, actionsContainer); - const actionsContainer = $('.peekview-actions').appendTo(this._headElement); const actionBarOptions = this._getActionBarOptions(); - this._actionbarWidget = new ActionBar(actionsContainer.getHTMLElement(), actionBarOptions); + this._actionbarWidget = new ActionBar(actionsContainer, actionBarOptions); this._disposables.push(this._actionbarWidget); this._actionbarWidget.push(new Action('peekview.close', nls.localize('label.close', "Close"), 'close-peekview-action', true, () => { @@ -146,15 +148,15 @@ export abstract class PeekViewWidget extends ZoneWidget { return {}; } - protected _onTitleClick(event: MouseEvent): void { + protected _onTitleClick(event: IMouseEvent): void { // implement me } public setTitle(primaryHeading: string, secondaryHeading?: string): void { - $(this._primaryHeading).safeInnerHtml(primaryHeading); + this._primaryHeading.innerHTML = strings.escape(primaryHeading); this._primaryHeading.setAttribute('aria-label', primaryHeading); if (secondaryHeading) { - $(this._secondaryHeading).safeInnerHtml(secondaryHeading); + this._secondaryHeading.innerHTML = strings.escape(secondaryHeading); } else { dom.clearNode(this._secondaryHeading); } @@ -162,7 +164,7 @@ export abstract class PeekViewWidget extends ZoneWidget { public setMetaTitle(value: string): void { if (value) { - $(this._metaHeading).safeInnerHtml(value); + this._metaHeading.innerHTML = strings.escape(value); } else { dom.clearNode(this._metaHeading); } diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index 47d501ae9ef..4afa93c925d 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -8,22 +8,27 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { TPromise } from 'vs/base/common/winjs.base'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Position } from 'vs/editor/common/core/position'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Position, IPosition } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorAction, ServicesAccessor, EditorAction, registerEditorContribution, registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Location, ReferenceProviderRegistry } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; import { PeekContext, getOuterEditor } from './peekViewWidget'; import { ReferencesController, RequestOptions, ctxReferenceSearchVisible } from './referencesController'; import { ReferencesModel, OneReference } from './referencesModel'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel } from 'vs/editor/common/model'; import { IListService } from 'vs/platform/list/browser/listService'; import { ctxReferenceWidgetSearchTreeFocused } from 'vs/editor/contrib/referenceSearch/referencesWidget'; +import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const defaultReferenceSearchOptions: RequestOptions = { getMetaTitle(model) { @@ -65,7 +70,8 @@ export class ReferenceAction extends EditorAction { EditorContextKeys.isInEmbeddedEditor.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyCode.F12 + primary: KeyMod.Shift | KeyCode.F12, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: 'navigation', @@ -81,7 +87,7 @@ export class ReferenceAction extends EditorAction { } let range = editor.getSelection(); let model = editor.getModel(); - let references = provideReferences(model, range.getStartPosition()).then(references => new ReferencesModel(references)); + let references = createCancelablePromise(token => provideReferences(model, range.getStartPosition(), token).then(references => new ReferencesModel(references))); controller.toggleWidget(range, references, defaultReferenceSearchOptions); } } @@ -90,6 +96,78 @@ registerEditorContribution(ReferenceController); registerEditorAction(ReferenceAction); +let findReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resource: URI, position: IPosition) => { + if (!(resource instanceof URI)) { + throw new Error('illegal argument, uri'); + } + if (!position) { + throw new Error('illegal argument, position'); + } + + const codeEditorService = accessor.get(ICodeEditorService); + return codeEditorService.openCodeEditor({ resource }, codeEditorService.getFocusedCodeEditor()).then(control => { + if (!isCodeEditor(control)) { + return undefined; + } + + let controller = ReferencesController.get(control); + if (!controller) { + return undefined; + } + + let references = createCancelablePromise(token => provideReferences(control.getModel(), Position.lift(position), token).then(references => new ReferencesModel(references))); + let range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + return TPromise.as(controller.toggleWidget(range, references, defaultReferenceSearchOptions)); + }); +}; + +let showReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resource: URI, position: IPosition, references: Location[]) => { + if (!(resource instanceof URI)) { + throw new Error('illegal argument, uri expected'); + } + + if (!references) { + throw new Error('missing references'); + } + + const codeEditorService = accessor.get(ICodeEditorService); + return codeEditorService.openCodeEditor({ resource }, codeEditorService.getFocusedCodeEditor()).then(control => { + if (!isCodeEditor(control)) { + return undefined; + } + + let controller = ReferencesController.get(control); + if (!controller) { + return undefined; + } + + return TPromise.as(controller.toggleWidget( + new Range(position.lineNumber, position.column, position.lineNumber, position.column), + createCancelablePromise(_ => Promise.resolve(new ReferencesModel(references))), + defaultReferenceSearchOptions)).then(() => true); + }); +}; + +// register commands + +CommandsRegistry.registerCommand({ + id: 'editor.action.findReferences', + handler: findReferencesCommand +}); + +CommandsRegistry.registerCommand({ + id: 'editor.action.showReferences', + handler: showReferencesCommand, + description: { + description: 'Show references at a position in a file', + args: [ + { name: 'uri', description: 'The text document in which to show references', constraint: URI }, + { name: 'position', description: 'The position at which to show', constraint: Position.isIPosition }, + { name: 'locations', description: 'An array of locations.', constraint: Array }, + ] + } +}); + function closeActiveReferenceSearch(accessor: ServicesAccessor, args: any) { withController(accessor, controller => controller.closeWidget()); } @@ -119,7 +197,7 @@ function withController(accessor: ServicesAccessor, fn: (controller: ReferencesC KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToNextReference', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, primary: KeyCode.F4, when: ctxReferenceSearchVisible, handler(accessor) { @@ -131,7 +209,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToNextReferenceFromEmbeddedEditor', - weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + weight: KeybindingWeight.EditorContrib + 50, primary: KeyCode.F4, when: PeekContext.inPeekEditor, handler(accessor) { @@ -143,7 +221,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToPreviousReference', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, primary: KeyMod.Shift | KeyCode.F4, when: ctxReferenceSearchVisible, handler(accessor) { @@ -155,7 +233,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToPreviousReferenceFromEmbeddedEditor', - weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + weight: KeybindingWeight.EditorContrib + 50, primary: KeyMod.Shift | KeyCode.F4, when: PeekContext.inPeekEditor, handler(accessor) { @@ -167,7 +245,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeReferenceSearch', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(ctxReferenceSearchVisible, ContextKeyExpr.not('config.editor.stablePeek')), @@ -176,7 +254,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeReferenceSearchEditor', - weight: KeybindingsRegistry.WEIGHT.editorContrib(-101), + weight: KeybindingWeight.EditorContrib - 101, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek')), @@ -185,7 +263,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'openReferenceToSide', - weight: KeybindingsRegistry.WEIGHT.editorContrib(), + weight: KeybindingWeight.EditorContrib, primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { primary: KeyMod.WinCtrl | KeyCode.Enter @@ -194,13 +272,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: openReferenceToSide }); -export function provideReferences(model: ITextModel, position: Position): TPromise { +export function provideReferences(model: ITextModel, position: Position, token: CancellationToken): Promise { // collect references from all providers const promises = ReferenceProviderRegistry.ordered(model).map(provider => { - return asWinJsPromise((token) => { - return provider.provideReferences(model, position, { includeDeclaration: true }, token); - }).then(result => { + return Promise.resolve(provider.provideReferences(model, position, { includeDeclaration: true }, token)).then(result => { if (Array.isArray(result)) { return result; } @@ -210,7 +286,7 @@ export function provideReferences(model: ITextModel, position: Position): TPromi }); }); - return TPromise.join(promises).then(references => { + return Promise.all(promises).then(references => { let result: Location[] = []; for (let ref of references) { if (ref) { @@ -221,4 +297,4 @@ export function provideReferences(model: ITextModel, position: Position): TPromi }); } -registerDefaultLanguageCommand('_executeReferenceProvider', provideReferences); +registerDefaultLanguageCommand('_executeReferenceProvider', (model, position) => provideReferences(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/referenceSearch/referencesController.ts b/src/vs/editor/contrib/referenceSearch/referencesController.ts index 4f0182c8d6c..32b8746b768 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesController.ts @@ -9,22 +9,19 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ReferencesModel } from './referencesModel'; import { ReferenceWidget, LayoutData } from './referencesWidget'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Position } from 'vs/editor/common/core/position'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Location } from 'vs/editor/common/modes'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CancelablePromise } from 'vs/base/common/async'; export const ctxReferenceSearchVisible = new RawContextKey('referenceSearchVisible', false); @@ -55,14 +52,10 @@ export abstract class ReferencesController implements editorCommon.IEditorContri editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @ICodeEditorService private readonly _editorService: ICodeEditorService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, @INotificationService private readonly _notificationService: INotificationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IStorageService private readonly _storageService: IStorageService, - @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @optional(IEnvironmentService) private _environmentService: IEnvironmentService ) { this._editor = editor; this._referenceSearchVisible = ctxReferenceSearchVisible.bindTo(contextKeyService); @@ -73,14 +66,16 @@ export abstract class ReferencesController implements editorCommon.IEditorContri } public dispose(): void { - if (this._widget) { - this._widget.dispose(); - this._widget = null; - } + this._referenceSearchVisible.reset(); + dispose(this._disposables); + dispose(this._widget); + dispose(this._model); + this._widget = null; + this._model = null; this._editor = null; } - public toggleWidget(range: Range, modelPromise: TPromise, options: RequestOptions): void { + public toggleWidget(range: Range, modelPromise: CancelablePromise, options: RequestOptions): void { // close current widget and return early is position didn't change let widgetPosition: Position; @@ -103,7 +98,7 @@ export abstract class ReferencesController implements editorCommon.IEditorContri })); const storageKey = 'peekViewLayout'; const data = JSON.parse(this._storageService.get(storageKey, undefined, '{}')); - this._widget = new ReferenceWidget(this._editor, this._defaultTreeKeyboardSupport, data, this._textModelResolverService, this._contextService, this._themeService, this._instantiationService, this._environmentService); + this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data); this._widget.setTitle(nls.localize('labelLoading', "Loading...")); this._widget.show(range); this._disposables.push(this._widget.onDidClose(() => { @@ -189,16 +184,12 @@ export abstract class ReferencesController implements editorCommon.IEditorContri } public closeWidget(): void { - if (this._widget) { - this._widget.dispose(); - this._widget = null; - } + dispose(this._widget); + this._widget = null; this._referenceSearchVisible.reset(); this._disposables = dispose(this._disposables); - if (this._model) { - this._model.dispose(); - this._model = null; - } + dispose(this._model); + this._model = null; this._editor.focus(); this._requestIdPool += 1; // Cancel pending requests } diff --git a/src/vs/editor/contrib/referenceSearch/referencesModel.ts b/src/vs/editor/contrib/referenceSearch/referencesModel.ts index 81865714ad8..5f529ae0017 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesModel.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesModel.ts @@ -6,10 +6,10 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { basename, dirname } from 'vs/base/common/paths'; +import { basename } from 'vs/base/common/paths'; import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { TPromise } from 'vs/base/common/winjs.base'; import { Range, IRange } from 'vs/editor/common/core/range'; @@ -18,52 +18,32 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { Position } from 'vs/editor/common/core/position'; export class OneReference { - private _id: string; - private _onRefChanged = new Emitter(); + readonly id: string; + private readonly _onRefChanged = new Emitter(); readonly onRefChanged: Event = this._onRefChanged.event; constructor( - private _parent: FileReferences, + readonly parent: FileReferences, private _range: IRange ) { - this._id = defaultGenerator.nextId(); + this.id = defaultGenerator.nextId(); } - public get id(): string { - return this._id; + get uri(): URI { + return this.parent.uri; } - public get model(): FileReferences { - return this._parent; - } - - public get parent(): FileReferences { - return this._parent; - } - - public get uri(): URI { - return this._parent.uri; - } - - public get name(): string { - return this._parent.name; - } - - public get directory(): string { - return this._parent.directory; - } - - public get range(): IRange { + get range(): IRange { return this._range; } - public set range(value: IRange) { + set range(value: IRange) { this._range = value; this._onRefChanged.fire(this); } - public getAriaMessage(): string { + getAriaMessage(): string { return localize( 'aria.oneReference', "symbol in {0} on line {1} at column {2}", basename(this.uri.fsPath), this.range.startLineNumber, this.range.startColumn @@ -73,14 +53,17 @@ export class OneReference { export class FilePreview implements IDisposable { - constructor(private _modelReference: IReference) { - + constructor( + private readonly _modelReference: IReference + ) { } - private get _model() { return this._modelReference.object.textEditorModel; } + dispose(): void { + dispose(this._modelReference); + } - public preview(range: IRange, n: number = 8): { before: string; inside: string; after: string } { - const model = this._model; + preview(range: IRange, n: number = 8): { before: string; inside: string; after: string } { + const model = this._modelReference.object.textEditorModel; if (!model) { return undefined; @@ -99,13 +82,6 @@ export class FilePreview implements IDisposable { return ret; } - - dispose(): void { - if (this._modelReference) { - this._modelReference.dispose(); - this._modelReference = null; - } - } } export class FileReferences implements IDisposable { @@ -115,39 +91,31 @@ export class FileReferences implements IDisposable { private _resolved: boolean; private _loadFailure: any; - constructor(private _parent: ReferencesModel, private _uri: URI) { + constructor(private readonly _parent: ReferencesModel, private _uri: URI) { this._children = []; } - public get id(): string { + get id(): string { return this._uri.toString(); } - public get parent(): ReferencesModel { + get parent(): ReferencesModel { return this._parent; } - public get children(): OneReference[] { + get children(): OneReference[] { return this._children; } - public get uri(): URI { + get uri(): URI { return this._uri; } - public get name(): string { - return basename(this.uri.fsPath); - } - - public get directory(): string { - return dirname(this.uri.fsPath); - } - - public get preview(): FilePreview { + get preview(): FilePreview { return this._preview; } - public get failure(): any { + get failure(): any { return this._loadFailure; } @@ -160,7 +128,7 @@ export class FileReferences implements IDisposable { } } - public resolve(textModelResolverService: ITextModelService): TPromise { + resolve(textModelResolverService: ITextModelService): TPromise { if (this._resolved) { return TPromise.as(this); @@ -198,11 +166,11 @@ export class FileReferences implements IDisposable { export class ReferencesModel implements IDisposable { private readonly _disposables: IDisposable[]; - private _groups: FileReferences[] = []; - private _references: OneReference[] = []; - private _onDidChangeReferenceRange = new Emitter(); + readonly groups: FileReferences[] = []; + readonly references: OneReference[] = []; - onDidChangeReferenceRange: Event = this._onDidChangeReferenceRange.event; + readonly _onDidChangeReferenceRange = new Emitter(); + readonly onDidChangeReferenceRange: Event = this._onDidChangeReferenceRange.event; constructor(references: Location[]) { this._disposables = []; @@ -223,22 +191,14 @@ export class ReferencesModel implements IDisposable { let oneRef = new OneReference(current, ref.range); this._disposables.push(oneRef.onRefChanged((e) => this._onDidChangeReferenceRange.fire(e))); - this._references.push(oneRef); + this.references.push(oneRef); current.children.push(oneRef); } } } - public get empty(): boolean { - return this._groups.length === 0; - } - - public get references(): OneReference[] { - return this._references; - } - - public get groups(): FileReferences[] { - return this._groups; + get empty(): boolean { + return this.groups.length === 0; } getAriaMessage(): string { @@ -253,7 +213,7 @@ export class ReferencesModel implements IDisposable { } } - public nextOrPreviousReference(reference: OneReference, next: boolean): OneReference { + nextOrPreviousReference(reference: OneReference, next: boolean): OneReference { let { parent } = reference; @@ -281,9 +241,9 @@ export class ReferencesModel implements IDisposable { } } - public nearestReference(resource: URI, position: Position): OneReference { + nearestReference(resource: URI, position: Position): OneReference { - const nearest = this._references.map((ref, idx) => { + const nearest = this.references.map((ref, idx) => { return { idx, prefixLen: strings.commonPrefixLength(ref.uri.toString(), resource.toString()), @@ -304,14 +264,15 @@ export class ReferencesModel implements IDisposable { })[0]; if (nearest) { - return this._references[nearest.idx]; + return this.references[nearest.idx]; } return undefined; } dispose(): void { - this._groups = dispose(this._groups); + dispose(this.groups); dispose(this._disposables); + this.groups.length = 0; this._disposables.length = 0; } diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index ea315ba3b73..c43976cf94a 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -4,46 +4,45 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import 'vs/css!./media/referencesWidget'; -import * as nls from 'vs/nls'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { getPathLabel } from 'vs/base/common/labels'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import * as strings from 'vs/base/common/strings'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { Color } from 'vs/base/common/color'; -import { $, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; -import { Sash, ISashEvent, IVerticalSashLayoutProvider } from 'vs/base/browser/ui/sash/sash'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { GestureEvent } from 'vs/base/browser/touch'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { FileLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { ISashEvent, IVerticalSashLayoutProvider, Sash } from 'vs/base/browser/ui/sash/sash'; +import { Color } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import * as strings from 'vs/base/common/strings'; +import { TPromise } from 'vs/base/common/winjs.base'; import * as tree from 'vs/base/parts/tree/browser/tree'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { TextModel, ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; +import 'vs/css!./media/referencesWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +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 * as nls from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { activeContrastBorder, contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PeekViewWidget } from './peekViewWidget'; import { FileReferences, OneReference, ReferencesModel } from './referencesModel'; -import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { registerColor, activeContrastBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant, ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import URI from 'vs/base/common/uri'; -import { TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model'; -import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Location } from 'vs/editor/common/modes'; -import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; + class DecorationsManager implements IDisposable { @@ -274,9 +273,9 @@ class Controller extends WorkbenchTreeController { private _expandCollapse(tree: tree.ITree, element: any): boolean { if (tree.isExpanded(element)) { - tree.collapse(element).done(null, onUnexpectedError); + tree.collapse(element).then(null, onUnexpectedError); } else { - tree.expand(element).done(null, onUnexpectedError); + tree.expand(element).then(null, onUnexpectedError); } return true; } @@ -294,22 +293,21 @@ class Controller extends WorkbenchTreeController { class FileReferencesTemplate { - readonly file: FileLabel; + readonly file: IconLabel; readonly badge: CountBadge; readonly dispose: () => void; constructor( container: HTMLElement, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @optional(IEnvironmentService) private _environmentService: IEnvironmentService, + @ILabelService private readonly _uriLabel: ILabelService, @IThemeService themeService: IThemeService, ) { const parent = document.createElement('div'); dom.addClass(parent, 'reference-file'); container.appendChild(parent); - this.file = new FileLabel(parent, URI.parse('no:file'), this._contextService, this._environmentService); + this.file = new IconLabel(parent); - this.badge = new CountBadge($('.count').appendTo(parent).getHTMLElement()); + this.badge = new CountBadge(dom.append(parent, dom.$('.count'))); const styler = attachBadgeStyler(this.badge, themeService); this.dispose = () => { @@ -319,7 +317,8 @@ class FileReferencesTemplate { } set(element: FileReferences) { - this.file.setFile(element.uri, this._contextService, this._environmentService); + let parent = dirname(element.uri); + this.file.setValue(getBaseLabel(element.uri), parent ? this._uriLabel.getUriLabel(parent, true) : undefined, { title: this._uriLabel.getUriLabel(element.uri) }); const len = element.children.length; this.badge.setCount(len); if (element.failure) { @@ -367,9 +366,8 @@ class Renderer implements tree.IRenderer { }; constructor( - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IThemeService private readonly _themeService: IThemeService, - @optional(IEnvironmentService) private _environmentService: IEnvironmentService, + @ILabelService private readonly _uriLabel: ILabelService, ) { // } @@ -389,7 +387,7 @@ class Renderer implements tree.IRenderer { renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement) { if (templateId === Renderer._ids.FileReferences) { - return new FileReferencesTemplate(container, this._contextService, this._environmentService, this._themeService); + return new FileReferencesTemplate(container, this._uriLabel, this._themeService); } else if (templateId === Renderer._ids.OneReference) { return new OneReferenceTemplate(container); } @@ -518,23 +516,22 @@ export class ReferenceWidget extends PeekViewWidget { private _onDidSelectReference = new Emitter(); private _tree: WorkbenchTree; - private _treeContainer: Builder; + private _treeContainer: HTMLElement; private _sash: VSash; private _preview: ICodeEditor; private _previewModelReference: IReference; private _previewNotAvailableMessage: TextModel; - private _previewContainer: Builder; - private _messageContainer: Builder; + private _previewContainer: HTMLElement; + private _messageContainer: HTMLElement; constructor( editor: ICodeEditor, private _defaultTreeKeyboardSupport: boolean, public layoutData: LayoutData, - private _textModelResolverService: ITextModelService, - private _contextService: IWorkspaceContextService, - themeService: IThemeService, - private _instantiationService: IInstantiationService, - private _environmentService: IEnvironmentService + @IThemeService themeService: IThemeService, + @ITextModelService private _textModelResolverService: ITextModelService, + @IInstantiationService private _instantiationService: IInstantiationService, + @ILabelService private _uriLabel: ILabelService ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); @@ -574,7 +571,7 @@ export class ReferenceWidget extends PeekViewWidget { this._tree.domFocus(); } - protected _onTitleClick(e: MouseEvent): void { + protected _onTitleClick(e: IMouseEvent): void { if (this._preview && this._preview.getModel()) { this._onDidSelectReference.fire({ element: this._getFocusedReference(), @@ -585,96 +582,88 @@ export class ReferenceWidget extends PeekViewWidget { } protected _fillBody(containerElement: HTMLElement): void { - let container = $(containerElement); - this.setCssClass('reference-zone-widget'); // message pane - container.div({ 'class': 'messages' }, div => { - this._messageContainer = div.hide(); - }); + this._messageContainer = dom.append(containerElement, dom.$('div.messages')); + dom.hide(this._messageContainer); // editor - container.div({ 'class': 'preview inline' }, (div: Builder) => { - - let options: IEditorOptions = { - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false - }, - overviewRulerLanes: 2, - fixedOverflowWidgets: true, - minimap: { - enabled: false - } - }; - - this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, div.getHTMLElement(), options, this.editor); - this._previewContainer = div.hide(); - this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available")); - }); + this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline')); + let options: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false + }, + overviewRulerLanes: 2, + fixedOverflowWidgets: true, + minimap: { + enabled: false + } + }; + this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor); + dom.hide(this._previewContainer); + this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available")); // sash this._sash = new VSash(containerElement, this.layoutData.ratio || .8); this._sash.onDidChangePercentages(() => { let [left, right] = this._sash.percentages; - this._previewContainer.style({ width: left }); - this._treeContainer.style({ width: right }); + this._previewContainer.style.width = left; + this._treeContainer.style.width = right; this._preview.layout(); this._tree.layout(); this.layoutData.ratio = this._sash.ratio; }); // tree - container.div({ 'class': 'ref-tree inline' }, (div: Builder) => { - let controller = this._instantiationService.createInstance(Controller, { keyboardSupport: this._defaultTreeKeyboardSupport, clickBehavior: ClickBehavior.ON_MOUSE_UP /* our controller already deals with this */ }); - this._callOnDispose.push(controller); + this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); + let controller = this._instantiationService.createInstance(Controller, { keyboardSupport: this._defaultTreeKeyboardSupport, clickBehavior: ClickBehavior.ON_MOUSE_UP /* our controller already deals with this */ }); + this._callOnDispose.push(controller); - let config = { - dataSource: this._instantiationService.createInstance(DataSource), - renderer: this._instantiationService.createInstance(Renderer), - controller, - accessibilityProvider: new AriaProvider() - }; + let config = { + dataSource: this._instantiationService.createInstance(DataSource), + renderer: this._instantiationService.createInstance(Renderer), + controller, + accessibilityProvider: new AriaProvider() + }; - let options: tree.ITreeOptions = { - twistiePixels: 20, - ariaLabel: nls.localize('treeAriaLabel', "References") - }; + let treeOptions: tree.ITreeOptions = { + twistiePixels: 20, + ariaLabel: nls.localize('treeAriaLabel', "References") + }; - this._tree = this._instantiationService.createInstance(WorkbenchTree, div.getHTMLElement(), config, options); + this._tree = this._instantiationService.createInstance(WorkbenchTree, this._treeContainer, config, treeOptions); - ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService); + ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService); - // listen on selection and focus - let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => { - if (element instanceof OneReference) { - if (kind === 'show') { - this._revealReference(element, false); - } - this._onDidSelectReference.fire({ element, kind, source: 'tree' }); + // listen on selection and focus + let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => { + if (element instanceof OneReference) { + if (kind === 'show') { + this._revealReference(element, false); } - }; - this._disposables.push(this._tree.onDidChangeFocus(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - onEvent(event.focus, 'show'); // only handle events from keyboard, mouse/touch is handled by other listeners below - } - })); - this._disposables.push(this._tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - onEvent(event.selection[0], 'goto'); // only handle events from keyboard, mouse/touch is handled by other listeners below - } - })); - this._disposables.push(controller.onDidFocus(element => onEvent(element, 'show'))); - this._disposables.push(controller.onDidSelect(element => onEvent(element, 'goto'))); - this._disposables.push(controller.onDidOpenToSide(element => onEvent(element, 'side'))); - - this._treeContainer = div.hide(); - }); + this._onDidSelectReference.fire({ element, kind, source: 'tree' }); + } + }; + this._disposables.push(this._tree.onDidChangeFocus(event => { + if (event && event.payload && event.payload.origin === 'keyboard') { + onEvent(event.focus, 'show'); // only handle events from keyboard, mouse/touch is handled by other listeners below + } + })); + this._disposables.push(this._tree.onDidChangeSelection(event => { + if (event && event.payload && event.payload.origin === 'keyboard') { + onEvent(event.selection[0], 'goto'); // only handle events from keyboard, mouse/touch is handled by other listeners below + } + })); + this._disposables.push(controller.onDidFocus(element => onEvent(element, 'show'))); + this._disposables.push(controller.onDidSelect(element => onEvent(element, 'goto'))); + this._disposables.push(controller.onDidOpenToSide(element => onEvent(element, 'side'))); + dom.hide(this._treeContainer); } protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { @@ -686,9 +675,10 @@ export class ReferenceWidget extends PeekViewWidget { // set height/width const [left, right] = this._sash.percentages; - this._previewContainer.style({ height, width: left }); - this._treeContainer.style({ height, width: right }); - + this._previewContainer.style.height = height; + this._previewContainer.style.width = left; + this._treeContainer.style.height = height; + this._treeContainer.style.width = right; // forward this._tree.layout(heightInPixel); this._preview.layout(); @@ -705,7 +695,7 @@ export class ReferenceWidget extends PeekViewWidget { this._preview.layout(); } - public setSelection(selection: OneReference): TPromise { + public setSelection(selection: OneReference): Promise { return this._revealReference(selection, true).then(() => { // show in tree @@ -728,11 +718,12 @@ export class ReferenceWidget extends PeekViewWidget { if (this._model.empty) { this.setTitle(''); - this._messageContainer.innerHtml(nls.localize('noResults', "No results")).show(); + this._messageContainer.innerHTML = nls.localize('noResults', "No results"); + dom.show(this._messageContainer); return TPromise.as(void 0); } - this._messageContainer.hide(); + dom.hide(this._messageContainer); this._decorationsManager = new DecorationsManager(this._preview, this._model); this._disposeOnNewModel.push(this._decorationsManager); @@ -753,8 +744,8 @@ export class ReferenceWidget extends PeekViewWidget { // make sure things are rendered dom.addClass(this.container, 'results-loaded'); - this._treeContainer.show(); - this._previewContainer.show(); + dom.show(this._treeContainer); + dom.show(this._previewContainer); this._preview.layout(); this._tree.layout(); this.focus(); @@ -776,11 +767,11 @@ export class ReferenceWidget extends PeekViewWidget { return undefined; } - private async _revealReference(reference: OneReference, revealParent: boolean): TPromise { + private async _revealReference(reference: OneReference, revealParent: boolean): Promise { // Update widget header if (reference.uri.scheme !== Schemas.inMemory) { - this.setTitle(reference.name, getPathLabel(reference.directory, this._contextService, this._environmentService)); + this.setTitle(basenameOrAuthority(reference.uri), this._uriLabel.getUriLabel(dirname(reference.uri), false)); } else { this.setTitle(nls.localize('peekView.alternateTitle', "References")); } diff --git a/src/vs/editor/contrib/referenceSearch/test/referencesModel.test.ts b/src/vs/editor/contrib/referenceSearch/test/referencesModel.test.ts index ec10a2dd1c4..d6114952d08 100644 --- a/src/vs/editor/contrib/referenceSearch/test/referencesModel.test.ts +++ b/src/vs/editor/contrib/referenceSearch/test/referencesModel.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/referencesModel'; diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index a3939df7767..349b72d0416 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -18,18 +18,18 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import RenameInputField from './renameInputField'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation } from 'vs/editor/common/modes'; +import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation, Rejection } from 'vs/editor/common/modes'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; import { MessageController } from 'vs/editor/contrib/message/messageController'; import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; class RenameSkeleton { @@ -46,13 +46,13 @@ class RenameSkeleton { return this._provider.length > 0; } - async resolveRenameLocation(): TPromise { + async resolveRenameLocation(token: CancellationToken): Promise { let [provider] = this._provider; - let res: RenameLocation; + let res: RenameLocation & Rejection; if (provider.resolveRenameLocation) { - res = await asWinJsPromise(token => provider.resolveRenameLocation(this.model, this.position, token)); + res = await provider.resolveRenameLocation(this.model, this.position, token); } if (!res) { @@ -68,7 +68,7 @@ class RenameSkeleton { return res; } - async provideRenameEdits(newName: string, i: number = 0, rejects: string[] = [], position: Position = this.position): TPromise { + async provideRenameEdits(newName: string, i: number = 0, rejects: string[] = [], token: CancellationToken): Promise { if (i >= this._provider.length) { return { @@ -78,18 +78,18 @@ class RenameSkeleton { } let provider = this._provider[i]; - let result = await asWinJsPromise((token) => provider.provideRenameEdits(this.model, this.position, newName, token)); + let result = await provider.provideRenameEdits(this.model, this.position, newName, token); if (!result) { - return this.provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result."))); + return this.provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token); } else if (result.rejectReason) { - return this.provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason)); + return this.provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token); } return result; } } -export async function rename(model: ITextModel, position: Position, newName: string): TPromise { - return new RenameSkeleton(model, position).provideRenameEdits(newName); +export async function rename(model: ITextModel, position: Position, newName: string): Promise { + return new RenameSkeleton(model, position).provideRenameEdits(newName, undefined, undefined, CancellationToken.None); } // --- register actions and commands @@ -127,7 +127,7 @@ class RenameController implements IEditorContribution { return RenameController.ID; } - public async run(): TPromise { + public async run(token: CancellationToken): Promise { const position = this.editor.getPosition(); const skeleton = new RenameSkeleton(this.editor.getModel(), position); @@ -136,9 +136,9 @@ class RenameController implements IEditorContribution { return undefined; } - let loc: RenameLocation; + let loc: RenameLocation & Rejection; try { - loc = await skeleton.resolveRenameLocation(); + loc = await skeleton.resolveRenameLocation(token); } catch (e) { MessageController.get(this.editor).showMessage(e, position); return undefined; @@ -148,6 +148,11 @@ class RenameController implements IEditorContribution { return undefined; } + if (loc.rejectReason) { + MessageController.get(this.editor).showMessage(loc.rejectReason, position); + return undefined; + } + let selection = this.editor.getSelection(); let selectionStart = 0; let selectionEnd = loc.text.length; @@ -172,7 +177,7 @@ class RenameController implements IEditorContribution { const state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll); - const renameOperation = skeleton.provideRenameEdits(newNameOrFocusFlag, 0, [], Range.lift(loc.range).getStartPosition()).then(result => { + const renameOperation = Promise.resolve(skeleton.provideRenameEdits(newNameOrFocusFlag, 0, [], token).then(result => { if (result.rejectReason) { if (state.validate(this.editor)) { MessageController.get(this.editor).showMessage(result.rejectReason, this.editor.getPosition()); @@ -183,9 +188,6 @@ class RenameController implements IEditorContribution { } return this._bulkEditService.apply(result, { editor: this.editor }).then(result => { - if (result.selection) { - this.editor.setSelection(result.selection); - } // alert if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, newNameOrFocusFlag, result.ariaSummary)); @@ -194,15 +196,15 @@ class RenameController implements IEditorContribution { }, err => { this._notificationService.error(nls.localize('rename.failed', "Rename failed to execute.")); - return TPromise.wrapError(err); - }); + return Promise.reject(err); + })); this._progressService.showWhile(renameOperation, 250); return renameOperation; }, err => { this._renameInputVisible.reset(); - return TPromise.wrapError(err); + return Promise.reject(err); }); } @@ -227,7 +229,8 @@ export class RenameAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyCode.F2 + primary: KeyCode.F2, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: '1_modification', @@ -256,7 +259,7 @@ export class RenameAction extends EditorAction { run(accessor: ServicesAccessor, editor: ICodeEditor): TPromise { let controller = RenameController.get(editor); if (controller) { - return controller.run(); + return TPromise.wrap(controller.run(CancellationToken.None)); } return undefined; } @@ -272,7 +275,7 @@ registerEditorCommand(new RenameCommand({ precondition: CONTEXT_RENAME_INPUT_VISIBLE, handler: x => x.acceptRenameInput(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(99), + weight: KeybindingWeight.EditorContrib + 99, kbExpr: EditorContextKeys.focus, primary: KeyCode.Enter } @@ -283,7 +286,7 @@ registerEditorCommand(new RenameCommand({ precondition: CONTEXT_RENAME_INPUT_VISIBLE, handler: x => x.cancelRenameInput(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(99), + weight: KeybindingWeight.EditorContrib + 99, kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index c297a5ea47b..9c87716107c 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -8,7 +8,6 @@ import 'vs/css!./renameInputField'; import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; @@ -123,7 +122,7 @@ export default class RenameInputField implements IContentWidget, IDisposable { } } - public getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number): TPromise { + public getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number): Promise { this._position = new Position(where.startLineNumber, where.startColumn); this._inputField.value = value; @@ -139,7 +138,7 @@ export default class RenameInputField implements IContentWidget, IDisposable { this._hide(); }; - return new TPromise(resolve => { + return new Promise(resolve => { this._currentCancelInput = (focusEditor) => { this._currentAcceptInput = null; @@ -171,14 +170,12 @@ export default class RenameInputField implements IContentWidget, IDisposable { this._show(); - }, () => { - this._currentCancelInput(true); }).then(newValue => { always(); return newValue; }, err => { always(); - return TPromise.wrapError(err); + return Promise.reject(err); }); } diff --git a/src/vs/editor/contrib/smartSelect/smartSelect.ts b/src/vs/editor/contrib/smartSelect/smartSelect.ts index 7c25548d3c9..cfe14c52788 100644 --- a/src/vs/editor/contrib/smartSelect/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/smartSelect.ts @@ -16,6 +16,8 @@ import { registerEditorAction, ServicesAccessor, IActionOptions, EditorAction, r import { TokenSelectionSupport, ILogicalSelectionEntry } from './tokenSelectionSupport'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; // --- selection state machine @@ -173,7 +175,14 @@ class GrowSelectionAction extends AbstractSmartSelect { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.RightArrow, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow } + mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '1_basic', + title: nls.localize({ key: 'miSmartSelectGrow', comment: ['&& denotes a mnemonic'] }, "&&Expand Selection"), + order: 2 } }); } @@ -189,7 +198,14 @@ class ShrinkSelectionAction extends AbstractSmartSelect { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.LeftArrow, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow } + mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarSelectionMenu, + group: '1_basic', + title: nls.localize({ key: 'miSmartSelectShrink', comment: ['&& denotes a mnemonic'] }, "&&Shrink Selection"), + order: 3 } }); } diff --git a/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts b/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts index a06b506123b..fb406b259ef 100644 --- a/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { LanguageIdentifier } from 'vs/editor/common/modes'; @@ -115,4 +115,38 @@ suite('TokenSelectionSupport', () => { // new Range(3, 19, 3, 20) ]); }); + + test('getRangesToPosition #56886. Skip empty lines correctly.', () => { + + assertGetRangesToPosition([ + 'function a(bar, foo){', + '\tif (bar) {', + '', + '\t}', + '}' + ], 3, 1, [ + new Range(1, 1, 5, 2), + new Range(1, 21, 5, 2), + new Range(2, 1, 4, 3), + new Range(2, 11, 4, 3) + ]); + }); + + test('getRangesToPosition #56886. Do not skip lines with only whitespaces.', () => { + + assertGetRangesToPosition([ + 'function a(bar, foo){', + '\tif (bar) {', + ' ', + '\t}', + '}' + ], 3, 1, [ + new Range(1, 1, 5, 2), + new Range(1, 21, 5, 2), + new Range(2, 1, 4, 3), + new Range(2, 11, 4, 3), + new Range(3, 1, 4, 2), + new Range(3, 1, 3, 2) + ]); + }); }); diff --git a/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts b/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts index 0ab1ea198e3..e54debcbd23 100644 --- a/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts +++ b/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/editor/contrib/smartSelect/tokenTree.ts b/src/vs/editor/contrib/smartSelect/tokenTree.ts index 537f2675c9f..755bfb013ae 100644 --- a/src/vs/editor/contrib/smartSelect/tokenTree.ts +++ b/src/vs/editor/contrib/smartSelect/tokenTree.ts @@ -174,7 +174,7 @@ class ModelRawTokenScanner { this._model.forceTokenization(this._lineNumber); this._lineTokens = this._model.getLineTokens(this._lineNumber); this._tokenIndex = 0; - if (this._lineTokens.getCount() === 0) { + if (this._lineTokens.getLineContent().length === 0) { // Skip empty lines this._lineTokens = null; } diff --git a/src/vs/editor/contrib/snippet/snippet.md b/src/vs/editor/contrib/snippet/snippet.md index 403cda77438..ce571c614af 100644 --- a/src/vs/editor/contrib/snippet/snippet.md +++ b/src/vs/editor/contrib/snippet/snippet.md @@ -53,6 +53,26 @@ ${TM_FILENAME/(.*)\..+$/$1/} |-> resolves to the filename ``` +Placeholder-Transform +-- + +Like a Variable-Transform, a transformation of a placeholder allows changing the inserted text for the placeholder when moving to the next tab stop. +The inserted text is matched with the regular expression and the match or matches - depending on the options - are replaced with the specified replacement format text. +Every occurrence of a placeholder can define its own transformation independently using the value of the first placeholder. +The format for Placeholder-Transforms is the same as for Variable-Transforms. + +The following sample removes an underscore at the beginning of the text. `_transform` becomes `transform`. + +``` +${1/^_(.*)/$1/} + | | | |-> No options + | | | + | | |-> Replace it with the first capture group + | | + | |-> Regular expression to capture everything after the underscore + | + |-> Placeholder Index +``` Grammar -- @@ -61,12 +81,15 @@ Below is the EBNF for snippets. With `\` (backslash) you can escape `$`, `}` and ``` any ::= tabstop | placeholder | choice | variable | text -tabstop ::= '$' int | '${' int '}' +tabstop ::= '$' int + | '${' int '}' + | '${' int transform '}' placeholder ::= '${' int ':' any '}' choice ::= '${' int '|' text (',' text)* '|}' variable ::= '$' var | '${' var }' | '${' var ':' any '}' - | '${' var '/' regex '/' (format | text)+ '/' options '}' + | '${' var transform '}' +transform ::= '/' regex '/' (format | text)+ '/' options format ::= '$' int | '${' int '}' | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' | '${' int ':+' if '}' diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 83abd441a26..03e9a581b1a 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -5,22 +5,22 @@ 'use strict'; -import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { registerEditorContribution, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { SnippetSession } from './snippetSession'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest'; -import { ISuggestion } from 'vs/editor/common/modes'; -import { Selection } from 'vs/editor/common/core/selection'; -import { Range } from 'vs/editor/common/core/range'; -import { Choice } from 'vs/editor/contrib/snippet/snippetParser'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { repeat } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { EditorCommand, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ISuggestion } from 'vs/editor/common/modes'; +import { Choice } from 'vs/editor/contrib/snippet/snippetParser'; +import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; +import { SnippetSession } from './snippetSession'; export class SnippetController2 implements IEditorContribution { @@ -65,13 +65,14 @@ export class SnippetController2 implements IEditorContribution { insert( template: string, overwriteBefore: number = 0, overwriteAfter: number = 0, - undoStopBefore: boolean = true, undoStopAfter: boolean = true + undoStopBefore: boolean = true, undoStopAfter: boolean = true, + adjustWhitespace: boolean = true, ): void { // this is here to find out more about the yet-not-understood // error that sometimes happens when we fail to inserted a nested // snippet try { - this._doInsert(template, overwriteBefore, overwriteAfter, undoStopBefore, undoStopAfter); + this._doInsert(template, overwriteBefore, overwriteAfter, undoStopBefore, undoStopAfter, adjustWhitespace); } catch (e) { this.cancel(); @@ -85,7 +86,8 @@ export class SnippetController2 implements IEditorContribution { private _doInsert( template: string, overwriteBefore: number = 0, overwriteAfter: number = 0, - undoStopBefore: boolean = true, undoStopAfter: boolean = true + undoStopBefore: boolean = true, undoStopAfter: boolean = true, + adjustWhitespace: boolean = true, ): void { // don't listen while inserting the snippet @@ -98,10 +100,10 @@ export class SnippetController2 implements IEditorContribution { if (!this._session) { this._modelVersionId = this._editor.getModel().getAlternativeVersionId(); - this._session = new SnippetSession(this._editor, template, overwriteBefore, overwriteAfter); + this._session = new SnippetSession(this._editor, template, overwriteBefore, overwriteAfter, adjustWhitespace); this._session.insert(); } else { - this._session.merge(template, overwriteBefore, overwriteAfter); + this._session.merge(template, overwriteBefore, overwriteAfter, adjustWhitespace); } if (undoStopAfter) { @@ -205,6 +207,10 @@ export class SnippetController2 implements IEditorContribution { this._updateState(); } + isInSnippet(): boolean { + return this._inSnippet.get(); + } + getSessionEnclosingRange(): Range { if (this._session) { return this._session.getEnclosingRange(); @@ -223,7 +229,7 @@ registerEditorCommand(new CommandCtor({ precondition: ContextKeyExpr.and(SnippetController2.InSnippetMode, SnippetController2.HasNextTabstop), handler: ctrl => ctrl.next(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(30), + weight: KeybindingWeight.EditorContrib + 30, kbExpr: EditorContextKeys.editorTextFocus, primary: KeyCode.Tab } @@ -233,7 +239,7 @@ registerEditorCommand(new CommandCtor({ precondition: ContextKeyExpr.and(SnippetController2.InSnippetMode, SnippetController2.HasPrevTabstop), handler: ctrl => ctrl.prev(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(30), + weight: KeybindingWeight.EditorContrib + 30, kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyCode.Tab } @@ -243,7 +249,7 @@ registerEditorCommand(new CommandCtor({ precondition: SnippetController2.InSnippetMode, handler: ctrl => ctrl.cancel(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(30), + weight: KeybindingWeight.EditorContrib + 30, kbExpr: EditorContextKeys.editorTextFocus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] @@ -255,7 +261,7 @@ registerEditorCommand(new CommandCtor({ precondition: SnippetController2.InSnippetMode, handler: ctrl => ctrl.finish(), // kbOpts: { - // weight: KeybindingsRegistry.WEIGHT.editorContrib(30), + // weight: KeybindingWeight.EditorContrib + 30, // kbExpr: EditorContextKeys.textFocus, // primary: KeyCode.Enter, // } diff --git a/src/vs/editor/contrib/snippet/snippetParser.ts b/src/vs/editor/contrib/snippet/snippetParser.ts index 2ee46939849..c839cffd343 100644 --- a/src/vs/editor/contrib/snippet/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/snippetParser.ts @@ -7,7 +7,7 @@ import { CharCode } from 'vs/base/common/charCode'; -export enum TokenType { +export const enum TokenType { Dollar, Colon, Comma, @@ -214,8 +214,11 @@ export class Text extends Marker { } } -export class Placeholder extends Marker { +export abstract class TransformableMarker extends Marker { + public transform: Transform; +} +export class Placeholder extends TransformableMarker { static compareByIndex(a: Placeholder, b: Placeholder): number { if (a.index === b.index) { return 0; @@ -247,17 +250,26 @@ export class Placeholder extends Marker { } toTextmateString(): string { - if (this.children.length === 0) { + let transformString = ''; + if (this.transform) { + transformString = this.transform.toTextmateString(); + } + if (this.children.length === 0 && !this.transform) { return `\$${this.index}`; + } else if (this.children.length === 0) { + return `\${${this.index}${transformString}}`; } else if (this.choice) { - return `\${${this.index}|${this.choice.toTextmateString()}|}`; + return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`; } else { - return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}}`; + return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; } } clone(): Placeholder { let ret = new Placeholder(this.index); + if (this.transform) { + ret.transform = this.transform.clone(); + } ret._children = this.children.map(child => child.clone()); return ret; } @@ -322,7 +334,7 @@ export class Transform extends Marker { } toTextmateString(): string { - return `/${Text.escape(this.regexp.source)}/${this.children.map(c => c.toTextmateString())}/${this.regexp.ignoreCase ? 'i' : ''}`; + return `/${Text.escape(this.regexp.source)}/${this.children.map(c => c.toTextmateString())}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}`; } clone(): Transform { @@ -384,7 +396,7 @@ export class FormatString extends Marker { } } -export class Variable extends Marker { +export class Variable extends TransformableMarker { constructor(public name: string) { super(); @@ -392,9 +404,8 @@ export class Variable extends Marker { resolve(resolver: VariableResolver): boolean { let value = resolver.resolve(this); - let [firstChild] = this._children; - if (firstChild instanceof Transform && this._children.length === 1) { - value = firstChild.resolve(value || ''); + if (this.transform) { + value = this.transform.resolve(value || ''); } if (value !== undefined) { this._children = [new Text(value)]; @@ -404,15 +415,22 @@ export class Variable extends Marker { } toTextmateString(): string { + let transformString = ''; + if (this.transform) { + transformString = this.transform.toTextmateString(); + } if (this.children.length === 0) { - return `\${${this.name}}`; + return `\${${this.name}${transformString}}`; } else { - return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}}`; + return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; } } clone(): Variable { const ret = new Variable(this.name); + if (this.transform) { + ret.transform = this.transform.clone(); + } ret._children = this.children.map(child => child.clone()); return ret; } @@ -580,6 +598,7 @@ export class SnippetParser { for (const placeholder of incompletePlaceholders) { if (placeholderDefaultValues.has(placeholder.index)) { const clone = new Placeholder(placeholder.index); + clone.transform = placeholder.transform; for (const child of placeholderDefaultValues.get(placeholder.index)) { clone.appendChild(child.clone()); } @@ -624,6 +643,9 @@ export class SnippetParser { let start = this._token; while (this._token.type !== type) { this._token = this._scanner.next(); + if (this._token.type === TokenType.EOF) { + return false; + } } let value = this._scanner.value.substring(start.pos, this._token.pos); this._token = this._scanner.next(); @@ -717,11 +739,13 @@ export class SnippetParser { continue; } - if (this._accept(TokenType.Pipe) && this._accept(TokenType.CurlyClose)) { - // ..|} -> done + if (this._accept(TokenType.Pipe)) { placeholder.appendChild(choice); - parent.appendChild(placeholder); - return true; + if (this._accept(TokenType.CurlyClose)) { + // ..|} -> done + parent.appendChild(placeholder); + return true; + } } } @@ -729,6 +753,16 @@ export class SnippetParser { return false; } + } else if (this._accept(TokenType.Forwardslash)) { + // ${1///} + if (this._parseTransform(placeholder)) { + parent.appendChild(placeholder); + return true; + } + + this._backTo(token); + return false; + } else if (this._accept(TokenType.CurlyClose)) { // ${1} parent.appendChild(placeholder); @@ -829,7 +863,7 @@ export class SnippetParser { } } - private _parseTransform(parent: Variable): boolean { + private _parseTransform(parent: TransformableMarker): boolean { // ...//} let transform = new Transform(); @@ -894,7 +928,7 @@ export class SnippetParser { return false; } - parent.appendChild(transform); + parent.transform = transform; return true; } diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 31be6b2ba16..65d1fe67f3d 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -5,21 +5,21 @@ 'use strict'; -import 'vs/css!./snippetSession'; -import { getLeadingWhitespace } from 'vs/base/common/strings'; -import { ITextModel, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { TextmateSnippet, Placeholder, Choice, SnippetParser } from './snippetParser'; -import { Selection } from 'vs/editor/common/core/selection'; -import { Range } from 'vs/editor/common/core/range'; -import { IPosition } from 'vs/editor/common/core/position'; import { groupBy } from 'vs/base/common/arrays'; import { dispose } from 'vs/base/common/lifecycle'; -import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver } from './snippetVariables'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { getLeadingWhitespace } from 'vs/base/common/strings'; +import 'vs/css!./snippetSession'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { Choice, Placeholder, SnippetParser, Text, TextmateSnippet } from './snippetParser'; +import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver } from './snippetVariables'; export class OneSnippet { @@ -87,6 +87,25 @@ export class OneSnippet { this._initDecorations(); + // Transform placeholder text if necessary + if (this._placeholderGroupsIdx >= 0) { + let operations: IIdentifiedSingleEditOperation[] = []; + + for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) { + // Check if the placeholder has a transformation + if (placeholder.transform) { + const id = this._placeholderDecorations.get(placeholder); + const range = this._editor.getModel().getDecorationRange(id); + const currentValue = this._editor.getModel().getValueInRange(range); + + operations.push(EditOperation.replaceMove(range, placeholder.transform.resolve(currentValue))); + } + } + if (operations.length > 0) { + this._editor.executeEdits('snippet.placeholderTransform', operations); + } + } + if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) { this._placeholderGroupsIdx += 1; @@ -165,6 +184,14 @@ export class OneSnippet { const id = this._placeholderDecorations.get(placeholder); const range = this._editor.getModel().getDecorationRange(id); + if (!range) { + // one of the placeholder lost its decoration and + // therefore we bail out and pretend the placeholder + // (with its mirrors) doesn't exist anymore. + result.delete(placeholder.index); + break; + } + ranges.push(range); } } @@ -244,17 +271,26 @@ export class OneSnippet { export class SnippetSession { - static adjustWhitespace(model: ITextModel, position: IPosition, template: string): string { - + static adjustWhitespace(model: ITextModel, position: IPosition, snippet: TextmateSnippet): void { const line = model.getLineContent(position.lineNumber); const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1); - const templateLines = template.split(/\r\n|\r|\n/); - for (let i = 1; i < templateLines.length; i++) { - let templateLeadingWhitespace = getLeadingWhitespace(templateLines[i]); - templateLines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + templateLines[i].substr(templateLeadingWhitespace.length); - } - return templateLines.join(model.getEOL()); + snippet.walk(marker => { + if (marker instanceof Text && !(marker.parent instanceof Choice)) { + // adjust indentation of text markers, except for choise elements + // which get adjusted when being selected + const lines = marker.value.split(/\r\n|\r|\n/); + for (let i = 1; i < lines.length; i++) { + let templateLeadingWhitespace = getLeadingWhitespace(lines[i]); + lines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + lines[i].substr(templateLeadingWhitespace.length); + } + const newValue = lines.join(model.getEOL()); + if (newValue !== marker.value) { + marker.parent.replace(marker, [new Text(newValue)]); + } + } + return true; + }); } static adjustSelection(model: ITextModel, selection: Selection, overwriteBefore: number, overwriteAfter: number): Selection { @@ -281,7 +317,7 @@ export class SnippetSession { return selection; } - static createEditsAndSnippets(editor: ICodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { + static createEditsAndSnippets(editor: ICodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { const model = editor.getModel(); const edits: IIdentifiedSingleEditOperation[] = []; @@ -324,19 +360,21 @@ export class SnippetSession { .setStartPosition(extensionBefore.startLineNumber, extensionBefore.startColumn) .setEndPosition(extensionAfter.endLineNumber, extensionAfter.endColumn); + const snippet = new SnippetParser().parse(template, true, enforceFinalTabstop); + // adjust the template string to match the indentation and // whitespace rules of this insert location (can be different for each cursor) const start = snippetSelection.getStartPosition(); - const adjustedTemplate = SnippetSession.adjustWhitespace(model, start, template); + if (adjustWhitespace) { + SnippetSession.adjustWhitespace(model, start, snippet); + } - const snippet = new SnippetParser() - .parse(adjustedTemplate, true, enforceFinalTabstop) - .resolveVariables(new CompositeSnippetVariableResolver([ - modelBasedVariableResolver, - new ClipboardBasedVariableResolver(clipboardService, idx, indexedSelections.length), - new SelectionBasedVariableResolver(model, selection), - new TimeBasedVariableResolver - ])); + snippet.resolveVariables(new CompositeSnippetVariableResolver([ + modelBasedVariableResolver, + new ClipboardBasedVariableResolver(clipboardService, idx, indexedSelections.length), + new SelectionBasedVariableResolver(model, selection), + new TimeBasedVariableResolver + ])); const offset = model.getOffsetAt(start) + delta; delta += snippet.toString().length - model.getValueLengthInRange(snippetSelection); @@ -356,13 +394,15 @@ export class SnippetSession { private readonly _templateMerges: [number, number, string][] = []; private readonly _overwriteBefore: number; private readonly _overwriteAfter: number; + private readonly _adjustWhitespace: boolean; private _snippets: OneSnippet[] = []; - constructor(editor: ICodeEditor, template: string, overwriteBefore: number = 0, overwriteAfter: number = 0) { + constructor(editor: ICodeEditor, template: string, overwriteBefore: number = 0, overwriteAfter: number = 0, adjustWhitespace: boolean = true) { this._editor = editor; this._template = template; this._overwriteBefore = overwriteBefore; this._overwriteAfter = overwriteAfter; + this._adjustWhitespace = adjustWhitespace; } dispose(): void { @@ -378,7 +418,7 @@ export class SnippetSession { const model = this._editor.getModel(); // make insert edit and start with first selections - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._overwriteBefore, this._overwriteAfter, false, this._adjustWhitespace); this._snippets = snippets; const selections = model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => { @@ -392,9 +432,9 @@ export class SnippetSession { this._editor.revealRange(selections[0]); } - merge(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void { + merge(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0, adjustWhitespace: boolean = true): void { this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, overwriteBefore, overwriteAfter, true); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, overwriteBefore, overwriteAfter, true, adjustWhitespace); this._editor.setSelections(this._editor.getModel().pushEditOperations(this._editor.getSelections(), edits, undoEdits => { diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index 20d5d3c96af..301c9dd1b81 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -240,6 +240,20 @@ suite('SnippetParser', () => { }); + test('Parser, placeholder transforms', function () { + assertTextAndMarker('${1///}', '', Placeholder); + assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder); + assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder); + + // tricky regex + assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder); + assertMarker('${1/regex\/format/options}', Text); + + // incomplete + assertTextAndMarker('${1///', '${1///', Text); + assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text); + }); + test('No way to escape forward slash in snippet regex #36715', function () { assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable); }); @@ -378,6 +392,41 @@ suite('SnippetParser', () => { assert.ok(marker[0] instanceof Variable); }); + test('Parser, transform example', () => { + let { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0'); + + //${1:name} + assert.ok(children[0] instanceof Placeholder); + assert.equal(children[0].children.length, 1); + assert.equal(children[0].children[0].toString(), 'name'); + assert.equal((children[0]).transform, undefined); + + // : + assert.ok(children[1] instanceof Text); + assert.equal(children[1].toString(), ' : '); + + //${2:type} + assert.ok(children[2] instanceof Placeholder); + assert.equal(children[2].children.length, 1); + assert.equal(children[2].children[0].toString(), 'type'); + + //${3/\\s:=(.*)/${1:+ :=}${1}/} + assert.ok(children[3] instanceof Placeholder); + assert.equal(children[3].children.length, 0); + assert.notEqual((children[3]).transform, undefined); + let transform = (children[3]).transform; + assert.equal(transform.regexp, '/\\s:=(.*)/'); + assert.equal(transform.children.length, 2); + assert.ok(transform.children[0] instanceof FormatString); + assert.equal((transform.children[0]).index, 1); + assert.equal((transform.children[0]).ifValue, ' :='); + assert.ok(transform.children[1] instanceof FormatString); + assert.equal((transform.children[1]).index, 1); + assert.ok(children[4] instanceof Text); + assert.equal(children[4].toString(), ';\n'); + + }); + test('Parser, default placeholder values', () => { assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder); @@ -393,6 +442,23 @@ suite('SnippetParser', () => { assert.equal(((p2).children[0]), 'err'); }); + test('Parser, default placeholder values and one transform', () => { + + assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder); + + const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children; + + assert.equal((p3).index, '1'); + assert.equal((p3).children.length, '1'); + assert.equal(((p3).children[0]), 'err'); + assert.equal((p3).transform, undefined); + + assert.equal((p4).index, '1'); + assert.equal((p4).children.length, '1'); + assert.equal(((p4).children[0]), 'err'); + assert.notEqual((p4).transform, undefined); + }); + test('Repeated snippet placeholder should always inherit, #31040', function () { assertText('${1:foo}-abc-$1', 'foo-abc-foo'); assertText('${1:foo}-abc-${1}', 'foo-abc-foo'); @@ -630,4 +696,20 @@ suite('SnippetParser', () => { const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}'); assertMarker(snippet, Variable); }); + + test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function () { + let transform = new Transform(); + transform.appendChild(new Text('bar')); + transform.regexp = new RegExp('foo', 'gi'); + assert.equal(transform.toTextmateString(), '/foo/bar/ig'); + }); + + test('Snippet parser freeze #53144', function () { + let snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}'); + assertMarker(snippet, Placeholder); + }); + + test('snippets variable not resolved in JSON proposal #52931', function () { + assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder); + }); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index 6c9ea6728db..361e3d57009 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -5,13 +5,14 @@ 'use strict'; import * as assert from 'assert'; -import { Selection } from 'vs/editor/common/core/selection'; -import { Range } from 'vs/editor/common/core/range'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; suite('SnippetSession', function () { @@ -41,8 +42,9 @@ suite('SnippetSession', function () { test('normalize whitespace', function () { function assertNormalized(position: IPosition, input: string, expected: string): void { - const actual = SnippetSession.adjustWhitespace(model, position, input); - assert.equal(actual, expected); + const snippet = new SnippetParser().parse(input); + SnippetSession.adjustWhitespace(model, position, snippet); + assert.equal(snippet.toTextmateString(), expected); } assertNormalized(new Position(1, 1), 'foo', 'foo'); @@ -51,6 +53,9 @@ suite('SnippetSession', function () { assertNormalized(new Position(2, 5), 'foo\r\tbar', 'foo\n bar'); assertNormalized(new Position(2, 3), 'foo\r\tbar', 'foo\n bar'); assertNormalized(new Position(2, 5), 'foo\r\tbar\nfoo', 'foo\n bar\n foo'); + + //Indentation issue with choice elements that span multiple lines #46266 + assertNormalized(new Position(2, 5), 'a\nb${1|foo,\nbar|}', 'a\n b${1|foo,\nbar|}'); }); test('adjust selection (overwrite[Before|After])', function () { @@ -120,6 +125,14 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(3, 1, 3, 1), new Selection(6, 5, 6, 5)); }); + test('snippets, newline NO whitespace adjust', () => { + + editor.setSelection(new Selection(2, 5, 2, 5)); + const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', 0, 0, false); + session.insert(); + assert.equal(editor.getModel().getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); + }); + test('snippets, selections -> next/prev', () => { const session = new SnippetSession(editor, 'f$1oo${2:bar}foo$0'); @@ -434,6 +447,121 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 6, 1, 25)); }); + test('snippets, transform', function () { + editor.getModel().setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + const session = new SnippetSession(editor, '${1/foo/bar/}$0'); + session.insert(); + assertSelections(editor, new Selection(1, 1, 1, 1)); + + editor.trigger('test', 'type', { text: 'foo' }); + session.next(); + + assert.equal(model.getValue(), 'bar'); + assert.equal(session.isAtLastPlaceholder, true); + assertSelections(editor, new Selection(1, 4, 1, 4)); + }); + + test('snippets, multi placeholder same index one transform', function () { + editor.getModel().setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + const session = new SnippetSession(editor, '$1 baz ${1/foo/bar/}$0'); + session.insert(); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(1, 6, 1, 6)); + + editor.trigger('test', 'type', { text: 'foo' }); + session.next(); + + assert.equal(model.getValue(), 'foo baz bar'); + assert.equal(session.isAtLastPlaceholder, true); + assertSelections(editor, new Selection(1, 12, 1, 12)); + }); + + test('snippets, transform example', function () { + editor.getModel().setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0'); + session.insert(); + + assertSelections(editor, new Selection(1, 1, 1, 5)); + editor.trigger('test', 'type', { text: 'clk' }); + session.next(); + + assertSelections(editor, new Selection(1, 7, 1, 11)); + editor.trigger('test', 'type', { text: 'std_logic' }); + session.next(); + + assertSelections(editor, new Selection(1, 16, 1, 16)); + session.next(); + + assert.equal(model.getValue(), 'clk : std_logic;\n'); + assert.equal(session.isAtLastPlaceholder, true); + assertSelections(editor, new Selection(2, 1, 2, 1)); + }); + + test('snippets, transform with indent', function () { + const snippet = [ + 'private readonly ${1} = new Emitter<$2>();', + 'readonly ${1/^_(.*)/$1/}: Event<$2> = this.$1.event;', + '$0' + ].join('\n'); + const expected = [ + '{', + '\tprivate readonly _prop = new Emitter();', + '\treadonly prop: Event = this._prop.event;', + '\t', + '}' + ].join('\n'); + const base = [ + '{', + '\t', + '}' + ].join('\n'); + + editor.getModel().setValue(base); + editor.getModel().updateOptions({ insertSpaces: false }); + editor.setSelection(new Selection(2, 2, 2, 2)); + + const session = new SnippetSession(editor, snippet); + session.insert(); + + assertSelections(editor, new Selection(2, 19, 2, 19), new Selection(3, 11, 3, 11), new Selection(3, 28, 3, 28)); + editor.trigger('test', 'type', { text: '_prop' }); + session.next(); + + assertSelections(editor, new Selection(2, 39, 2, 39), new Selection(3, 23, 3, 23)); + editor.trigger('test', 'type', { text: 'string' }); + session.next(); + + assert.equal(model.getValue(), expected); + assert.equal(session.isAtLastPlaceholder, true); + assertSelections(editor, new Selection(4, 2, 4, 2)); + + }); + + test('snippets, transform example hit if', function () { + editor.getModel().setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0'); + session.insert(); + + assertSelections(editor, new Selection(1, 1, 1, 5)); + editor.trigger('test', 'type', { text: 'clk' }); + session.next(); + + assertSelections(editor, new Selection(1, 7, 1, 11)); + editor.trigger('test', 'type', { text: 'std_logic' }); + session.next(); + + assertSelections(editor, new Selection(1, 16, 1, 16)); + editor.trigger('test', 'type', { text: ' := \'1\'' }); + session.next(); + + assert.equal(model.getValue(), 'clk : std_logic := \'1\';\n'); + assert.equal(session.isAtLastPlaceholder, true); + assertSelections(editor, new Selection(2, 1, 2, 1)); + }); + test('Snippet placeholder index incorrect after using 2+ snippets in a row that each end with a placeholder, #30769', function () { editor.getModel().setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 80cb27ce5ad..77715fd2e6d 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index 255750fe219..06208d010d0 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -8,7 +8,8 @@ import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore } from 'vs/base/common/filters'; import { isDisposable } from 'vs/base/common/lifecycle'; import { ISuggestResult, ISuggestSupport } from 'vs/editor/common/modes'; -import { ISuggestionItem, SnippetConfig } from './suggest'; +import { ISuggestionItem } from './suggest'; +import { InternalSuggestOptions, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions'; export interface ICompletionItem extends ISuggestionItem { matches?: number[]; @@ -46,8 +47,9 @@ const enum Refilter { export class CompletionModel { - private readonly _column: number; private readonly _items: ICompletionItem[]; + private readonly _column: number; + private readonly _options: InternalSuggestOptions; private readonly _snippetCompareFn = CompletionModel._compareCompletionItems; private _lineContext: LineContext; @@ -56,15 +58,16 @@ export class CompletionModel { private _isIncomplete: Set; private _stats: ICompletionStats; - constructor(items: ISuggestionItem[], column: number, lineContext: LineContext, snippetConfig?: SnippetConfig) { + constructor(items: ISuggestionItem[], column: number, lineContext: LineContext, options: InternalSuggestOptions = EDITOR_DEFAULTS.contribInfo.suggest) { this._items = items; this._column = column; + this._options = options; this._refilterKind = Refilter.All; this._lineContext = lineContext; - if (snippetConfig === 'top') { + if (options.snippets === 'top') { this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp; - } else if (snippetConfig === 'bottom') { + } else if (options.snippets === 'bottom') { this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown; } } @@ -146,8 +149,9 @@ export class CompletionModel { const target: typeof source = []; // picks a score function based on the number of - // items that we have to score/filter - const scoreFn = source.length > 2000 ? fuzzyScore : fuzzyScoreGracefulAggressive; + // items that we have to score/filter and based on the + // user-configuration + const scoreFn = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive; for (let i = 0; i < source.length; i++) { diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 8136074bc0f..0df5abe1e5e 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -2,14 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { sequence, asWinJsPromise } from 'vs/base/common/async'; +import { first2 } from 'vs/base/common/async'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { compareIgnoreCase } from 'vs/base/common/strings'; import { assign } from 'vs/base/common/objects'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; @@ -17,6 +15,7 @@ import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry, SuggestC import { Position, IPosition } from 'vs/editor/common/core/position'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false), @@ -31,20 +30,31 @@ export interface ISuggestionItem { suggestion: ISuggestion; container: ISuggestResult; support: ISuggestSupport; - resolve(): TPromise; + resolve(token: CancellationToken): Thenable; } export type SnippetConfig = 'top' | 'bottom' | 'inline' | 'none'; let _snippetSuggestSupport: ISuggestSupport; +export function getSnippetSuggestSupport(): ISuggestSupport { + return _snippetSuggestSupport; +} + export function setSnippetSuggestSupport(support: ISuggestSupport): ISuggestSupport { const old = _snippetSuggestSupport; _snippetSuggestSupport = support; return old; } -export function provideSuggestionItems(model: ITextModel, position: Position, snippetConfig: SnippetConfig = 'bottom', onlyFrom?: ISuggestSupport[], context?: SuggestContext): TPromise { +export function provideSuggestionItems( + model: ITextModel, + position: Position, + snippetConfig: SnippetConfig = 'bottom', + onlyFrom?: ISuggestSupport[], + context?: SuggestContext, + token: CancellationToken = CancellationToken.None +): Promise { const allSuggestions: ISuggestionItem[] = []; const acceptSuggestion = createSuggesionFilter(snippetConfig); @@ -64,50 +74,44 @@ export function provideSuggestionItems(model: ITextModel, position: Position, sn // add suggestions from contributed providers - providers are ordered in groups of // equal score and once a group produces a result the process stops let hasResult = false; - const factory = supports.map(supports => { - return () => { - // stop when we have a result - if (hasResult) { + const factory = supports.map(supports => () => { + // for each support in the group ask for suggestions + return Promise.all(supports.map(support => { + + if (!isFalsyOrEmpty(onlyFrom) && onlyFrom.indexOf(support) < 0) { return undefined; } - // for each support in the group ask for suggestions - return TPromise.join(supports.map(support => { - if (!isFalsyOrEmpty(onlyFrom) && onlyFrom.indexOf(support) < 0) { - return undefined; - } + return Promise.resolve(support.provideCompletionItems(model, position, suggestConext, token)).then(container => { - return asWinJsPromise(token => support.provideCompletionItems(model, position, suggestConext, token)).then(container => { + const len = allSuggestions.length; - const len = allSuggestions.length; + if (container && !isFalsyOrEmpty(container.suggestions)) { + for (let suggestion of container.suggestions) { + if (acceptSuggestion(suggestion)) { - if (container && !isFalsyOrEmpty(container.suggestions)) { - for (let suggestion of container.suggestions) { - if (acceptSuggestion(suggestion)) { + fixOverwriteBeforeAfter(suggestion, container); - fixOverwriteBeforeAfter(suggestion, container); - - allSuggestions.push({ - position, - container, - suggestion, - support, - resolve: createSuggestionResolver(support, suggestion, model, position) - }); - } + allSuggestions.push({ + position, + container, + suggestion, + support, + resolve: createSuggestionResolver(support, suggestion, model, position) + }); } } + } - if (len !== allSuggestions.length && support !== _snippetSuggestSupport) { - hasResult = true; - } + if (len !== allSuggestions.length && support !== _snippetSuggestSupport) { + hasResult = true; + } - }, onUnexpectedExternalError); - })); - }; + }, onUnexpectedExternalError); + })); }); - const result = sequence(factory).then(() => allSuggestions.sort(getSuggestionComparator(snippetConfig))); + const result = first2(factory, () => hasResult).then(() => allSuggestions.sort(getSuggestionComparator(snippetConfig))); // result.then(items => { // console.log(model.getWordUntilPosition(position), items.map(item => `${item.suggestion.label}, type=${item.suggestion.type}, incomplete?${item.container.incomplete}, overwriteBefore=${item.suggestion.overwriteBefore}`)); @@ -128,13 +132,13 @@ function fixOverwriteBeforeAfter(suggestion: ISuggestion, container: ISuggestRes } } -function createSuggestionResolver(provider: ISuggestSupport, suggestion: ISuggestion, model: ITextModel, position: Position): () => TPromise { - return () => { +function createSuggestionResolver(provider: ISuggestSupport, suggestion: ISuggestion, model: ITextModel, position: Position): (token: CancellationToken) => Promise { + return (token) => { if (typeof provider.resolveCompletionItem === 'function') { - return asWinJsPromise(token => provider.resolveCompletionItem(model, position, suggestion, token)) - .then(value => { assign(suggestion, value); }); + return Promise.resolve(provider.resolveCompletionItem(model, position, suggestion, token)).then(value => { assign(suggestion, value); }); + } else { + return Promise.resolve(void 0); } - return TPromise.as(void 0); }; } @@ -216,13 +220,13 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio return provideSuggestionItems(model, position).then(items => { for (const item of items) { if (resolving.length < maxItemsToResolve) { - resolving.push(item.resolve()); + resolving.push(item.resolve(CancellationToken.None)); } result.incomplete = result.incomplete || item.container.incomplete; result.suggestions.push(item.suggestion); } }).then(() => { - return TPromise.join(resolving); + return Promise.all(resolving); }).then(() => { return result; }); @@ -232,10 +236,16 @@ interface SuggestController extends IEditorContribution { triggerSuggest(onlyFrom?: ISuggestSupport[]): void; } -let _suggestions: ISuggestion[]; + let _provider = new class implements ISuggestSupport { + + onlyOnceSuggestions: ISuggestion[] = []; + provideCompletionItems(): ISuggestResult { - return _suggestions && { suggestions: _suggestions }; + let suggestions = this.onlyOnceSuggestions.slice(0); + let result = { suggestions }; + this.onlyOnceSuggestions.length = 0; + return result; } }; @@ -243,8 +253,7 @@ SuggestRegistry.register('*', _provider); export function showSimpleSuggestions(editor: ICodeEditor, suggestions: ISuggestion[]) { setTimeout(() => { - _suggestions = suggestions; + _provider.onlyOnceSuggestions.push(...suggestions); editor.getContribution('editor.contrib.suggestController').triggerSuggest([_provider]); - _suggestions = undefined; }, 0); } diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 1d1cf7b049c..f73149757ec 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -4,30 +4,30 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as nls from 'vs/nls'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ISuggestSupport } from 'vs/editor/common/modes'; -import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { Context as SuggestContext } from './suggest'; -import { SuggestModel, State } from './suggestModel'; -import { ICompletionItem } from './completionModel'; -import { SuggestWidget, ISelectedSuggestion } from './suggestWidget'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { SuggestMemories } from 'vs/editor/contrib/suggest/suggestMemory'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICompletionItem } from './completionModel'; +import { Context as SuggestContext } from './suggest'; +import { State, SuggestModel } from './suggestModel'; +import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; class AcceptOnCharacterOracle { @@ -88,6 +88,8 @@ export class SuggestController implements IEditorContribution { private _memory: SuggestMemories; private _toDispose: IDisposable[] = []; + private readonly _sticky = false; // for development purposes only + constructor( private _editor: ICodeEditor, @ICommandService private readonly _commandService: ICommandService, @@ -112,6 +114,11 @@ export class SuggestController implements IEditorContribution { this._widget.hideWidget(); } })); + this._toDispose.push(this._editor.onDidBlurEditorText(() => { + if (!this._sticky) { + this._model.cancel(); + } + })); // Manage the acceptSuggestionsOnEnter context key let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService); @@ -196,10 +203,12 @@ export class SuggestController implements IEditorContribution { const editorColumn = this._editor.getPosition().column; const columnDelta = editorColumn - position.column; + // pushing undo stops *before* additional text edits and + // *after* the main edit + this._editor.pushUndoStop(); + if (Array.isArray(suggestion.additionalTextEdits)) { - this._editor.pushUndoStop(); this._editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); - this._editor.pushUndoStop(); } // keep item in memory @@ -213,9 +222,13 @@ export class SuggestController implements IEditorContribution { SnippetController2.get(this._editor).insert( insertText, suggestion.overwriteBefore + columnDelta, - suggestion.overwriteAfter + suggestion.overwriteAfter, + false, false, + !suggestion.noWhitespaceAdjust ); + this._editor.pushUndoStop(); + if (!suggestion.command) { // done this._model.cancel(); @@ -226,7 +239,7 @@ export class SuggestController implements IEditorContribution { } else { // exec command, done - this._commandService.executeCommand(suggestion.command.id, ...suggestion.command.arguments).done(undefined, onUnexpectedError); + this._commandService.executeCommand(suggestion.command.id, ...suggestion.command.arguments).then(undefined, onUnexpectedError); this._model.cancel(); } @@ -320,7 +333,8 @@ export class TriggerSuggestAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Space, - mac: { primary: KeyMod.WinCtrl | KeyCode.Space } + mac: { primary: KeyMod.WinCtrl | KeyCode.Space }, + weight: KeybindingWeight.EditorContrib } }); } @@ -339,7 +353,7 @@ export class TriggerSuggestAction extends EditorAction { registerEditorContribution(SuggestController); registerEditorAction(TriggerSuggestAction); -const weight = KeybindingsRegistry.WEIGHT.editorContrib(90); +const weight = KeybindingWeight.EditorContrib + 90; const SuggestCommand = EditorCommand.bindToContribution(SuggestController.get); @@ -350,7 +364,7 @@ registerEditorCommand(new SuggestCommand({ handler: x => x.acceptSelectedSuggestion(), kbOpts: { weight: weight, - kbExpr: ContextKeyExpr.and(EditorContextKeys.textInputFocus, SnippetController2.InSnippetMode.toNegated()), + kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Tab } })); diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index 29c6afc906d..e278a619c96 100644 --- a/src/vs/editor/contrib/suggest/suggestMemory.ts +++ b/src/vs/editor/contrib/suggest/suggestMemory.ts @@ -13,9 +13,26 @@ import { RunOnceScheduler } from 'vs/base/common/async'; export abstract class Memory { - abstract memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void; + select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { + if (items.length === 0) { + return 0; + } + let topScore = items[0].score; + for (let i = 1; i < items.length; i++) { + const { score, suggestion } = items[i]; + if (score !== topScore) { + // stop when leaving the group of top matches + break; + } + if (suggestion.preselect) { + // stop when seeing an auto-select-item + return i; + } + } + return 0; + } - abstract select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number; + abstract memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void; abstract toJSON(): object; @@ -28,10 +45,6 @@ export class NoMemory extends Memory { // no-op } - select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { - return 0; - } - toJSON() { return undefined; } @@ -67,15 +80,15 @@ export class LRUMemory extends Memory { // that has been used in the past let { word } = model.getWordUntilPosition(pos); if (word.length !== 0) { - return 0; + return super.select(model, pos, items); } let lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1); if (/\s$/.test(lineSuffix)) { - return 0; + return super.select(model, pos, items); } - let res = 0; + let res = -1; let seq = -1; for (let i = 0; i < items.length; i++) { const { suggestion } = items[i]; @@ -86,7 +99,11 @@ export class LRUMemory extends Memory { res = i; } } - return res; + if (res === -1) { + return super.select(model, pos, items); + } else { + return res; + } } toJSON(): object { @@ -127,7 +144,7 @@ export class PrefixMemory extends Memory { select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { let { word } = model.getWordUntilPosition(pos); if (!word) { - return 0; + return super.select(model, pos, items); } let key = `${model.getLanguageIdentifier().language}/${word}`; let item = this._trie.get(key); @@ -142,7 +159,7 @@ export class PrefixMemory extends Memory { } } } - return 0; + return super.select(model, pos, items); } toJSON(): object { diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index 417451c9a35..cceda3b6cfd 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -5,12 +5,11 @@ 'use strict'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { TimeoutTimer } from 'vs/base/common/async'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; @@ -18,7 +17,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; import { ISuggestSupport, StandardTokenType, SuggestContext, SuggestRegistry, SuggestTriggerKind } from 'vs/editor/common/modes'; import { CompletionModel } from './completionModel'; -import { ISuggestionItem, getSuggestionComparator, provideSuggestionItems } from './suggest'; +import { ISuggestionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport } from './suggest'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; export interface ICancelEvent { readonly retrigger: boolean; @@ -89,11 +89,11 @@ export class SuggestModel implements IDisposable { private _toDispose: IDisposable[] = []; private _quickSuggestDelay: number; private _triggerCharacterListener: IDisposable; - private _triggerAutoSuggestPromise: TPromise; - private _triggerRefilter = new TimeoutTimer(); + private readonly _triggerQuickSuggest = new TimeoutTimer(); + private readonly _triggerRefilter = new TimeoutTimer(); private _state: State; - private _requestPromise: TPromise; + private _requestPromise: CancelablePromise; private _context: LineContext; private _currentSelection: Selection; @@ -109,7 +109,6 @@ export class SuggestModel implements IDisposable { constructor(editor: ICodeEditor) { this._editor = editor; this._state = State.Idle; - this._triggerAutoSuggestPromise = null; this._requestPromise = null; this._completionModel = null; this._context = null; @@ -135,16 +134,31 @@ export class SuggestModel implements IDisposable { this._toDispose.push(this._editor.onDidChangeCursorSelection(e => { this._onCursorChange(e); })); - this._toDispose.push(this._editor.onDidChangeModelContent(e => { + + let editorIsComposing = false; + this._toDispose.push(this._editor.onCompositionStart(() => { + editorIsComposing = true; + })); + this._toDispose.push(this._editor.onCompositionEnd(() => { + // refilter when composition ends + editorIsComposing = false; this._refilterCompletionItems(); })); + this._toDispose.push(this._editor.onDidChangeModelContent(() => { + // only filter completions when the editor isn't + // composing a character, e.g. ¨ + u makes ü but just + // ¨ cannot be used for filtering + if (!editorIsComposing) { + this._refilterCompletionItems(); + } + })); this._updateTriggerCharacters(); this._updateQuickSuggest(); } dispose(): void { - dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerCharacterListener, this._triggerRefilter]); + dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerCharacterListener, this._triggerQuickSuggest, this._triggerRefilter]); this._toDispose = dispose(this._toDispose); dispose(this._completionModel); this.cancel(); @@ -180,6 +194,7 @@ export class SuggestModel implements IDisposable { let set = supportsByTriggerCharacter[ch]; if (!set) { set = supportsByTriggerCharacter[ch] = new Set(); + set.add(getSnippetSuggestSupport()); } set.add(support); } @@ -208,9 +223,9 @@ export class SuggestModel implements IDisposable { this._triggerRefilter.cancel(); - if (this._triggerAutoSuggestPromise) { - this._triggerAutoSuggestPromise.cancel(); - this._triggerAutoSuggestPromise = null; + if (this._triggerQuickSuggest) { + this._triggerQuickSuggest.cancel(); + } if (this._requestPromise) { @@ -264,48 +279,58 @@ export class SuggestModel implements IDisposable { if (this._state === State.Idle) { - // trigger 24x7 IntelliSense when idle, enabled, when cursor - // moved RIGHT, and when at a good position - if (this._editor.getConfiguration().contribInfo.quickSuggestions !== false - && (prevSelection.containsRange(this._currentSelection) - || prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) - ) { - this.cancel(); - - this._triggerAutoSuggestPromise = TPromise.timeout(this._quickSuggestDelay); - this._triggerAutoSuggestPromise.then(() => { - if (LineContext.shouldAutoTrigger(this._editor)) { - const model = this._editor.getModel(); - const pos = this._editor.getPosition(); - - if (!model) { - return; - } - // validate enabled now - const { quickSuggestions } = this._editor.getConfiguration().contribInfo; - if (quickSuggestions === false) { - return; - } else if (quickSuggestions === true) { - // all good - } else { - // Check the type of the token that triggered this - model.tokenizeIfCheap(pos.lineNumber); - const lineTokens = model.getLineTokens(pos.lineNumber); - const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); - const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other - || quickSuggestions.comments && tokenType === StandardTokenType.Comment - || quickSuggestions.strings && tokenType === StandardTokenType.String; - - if (!inValidScope) { - return; - } - } - - this.trigger({ auto: true }); - } - this._triggerAutoSuggestPromise = null; - }); + if (this._editor.getConfiguration().contribInfo.quickSuggestions === false) { + // not enabled + return; } + + if (!prevSelection.containsRange(this._currentSelection) && !prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) { + // cursor didn't move RIGHT + return; + } + + if (this._editor.getConfiguration().contribInfo.suggest.snippetsPreventQuickSuggestions && SnippetController2.get(this._editor).isInSnippet()) { + // no quick suggestion when in snippet mode + return; + } + + this.cancel(); + + this._triggerQuickSuggest.cancelAndSet(() => { + if (!LineContext.shouldAutoTrigger(this._editor)) { + return; + } + + const model = this._editor.getModel(); + const pos = this._editor.getPosition(); + if (!model) { + return; + } + // validate enabled now + const { quickSuggestions } = this._editor.getConfiguration().contribInfo; + if (quickSuggestions === false) { + return; + } else if (quickSuggestions === true) { + // all good + } else { + // Check the type of the token that triggered this + model.tokenizeIfCheap(pos.lineNumber); + const lineTokens = model.getLineTokens(pos.lineNumber); + const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); + const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other + || quickSuggestions.comments && tokenType === StandardTokenType.Comment + || quickSuggestions.strings && tokenType === StandardTokenType.String; + + if (!inValidScope) { + return; + } + } + + // we made it till here -> trigger now + this.trigger({ auto: true }); + + }, this._quickSuggestDelay); + } } @@ -355,11 +380,16 @@ export class SuggestModel implements IDisposable { suggestCtx = { triggerKind: SuggestTriggerKind.Invoke }; } - this._requestPromise = provideSuggestionItems(model, this._editor.getPosition(), - this._editor.getConfiguration().contribInfo.snippetSuggestions, + this._requestPromise = createCancelablePromise(token => provideSuggestionItems( + model, + this._editor.getPosition(), + this._editor.getConfiguration().contribInfo.suggest.snippets, onlyFrom, - suggestCtx - ).then(items => { + suggestCtx, + token + )); + + this._requestPromise.then(items => { this._requestPromise = null; if (this._state === State.Idle) { @@ -371,7 +401,7 @@ export class SuggestModel implements IDisposable { } if (!isFalsyOrEmpty(existingItems)) { - const cmpFn = getSuggestionComparator(this._editor.getConfiguration().contribInfo.snippetSuggestions); + const cmpFn = getSuggestionComparator(this._editor.getConfiguration().contribInfo.suggest.snippets); items = items.concat(existingItems).sort(cmpFn); } @@ -380,10 +410,12 @@ export class SuggestModel implements IDisposable { this._completionModel = new CompletionModel(items, this._context.column, { leadingLineContent: ctx.leadingLineContent, characterCountDelta: this._context ? ctx.column - this._context.column : 0 - }, this._editor.getConfiguration().contribInfo.snippetSuggestions); + }, + this._editor.getConfiguration().contribInfo.suggest + ); this._onNewContext(ctx); - }).then(null, onUnexpectedError); + }).catch(onUnexpectedError); } private _onNewContext(ctx: LineContext): void { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 0f907cf62d9..4c86b7a6a67 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -10,12 +10,11 @@ import * as nls from 'vs/nls'; import { createMatches } from 'vs/base/common/filters'; import * as strings from 'vs/base/common/strings'; import { Event, Emitter, chain } from 'vs/base/common/event'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -33,8 +32,9 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; -const sticky = false; // for development purposes const expandSuggestionDocsByDefault = false; const maxSuggestionsToShow = 12; @@ -110,10 +110,12 @@ class Renderer implements IRenderer { const fontFamily = configuration.fontInfo.fontFamily; const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize; const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight; + const fontWeight = configuration.fontInfo.fontWeight; const fontSizePx = `${fontSize}px`; const lineHeightPx = `${lineHeight}px`; data.root.style.fontSize = fontSizePx; + data.root.style.fontWeight = fontWeight; main.style.fontFamily = fontFamily; main.style.lineHeight = lineHeightPx; data.icon.style.height = lineHeightPx; @@ -172,7 +174,10 @@ class Renderer implements IRenderer { data.readMore.onmousedown = null; data.readMore.onclick = null; } + } + disposeElement(): void { + // noop } disposeTemplate(templateData: ISuggestionTemplateData): void { @@ -327,10 +332,12 @@ class SuggestionDetails { const fontFamily = configuration.fontInfo.fontFamily; const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize; const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight; + const fontWeight = configuration.fontInfo.fontWeight; const fontSizePx = `${fontSize}px`; const lineHeightPx = `${lineHeight}px`; this.el.style.fontSize = fontSizePx; + this.el.style.fontWeight = fontWeight; this.type.style.fontFamily = fontFamily; this.close.style.height = lineHeightPx; this.close.style.width = lineHeightPx; @@ -348,7 +355,7 @@ export interface ISelectedSuggestion { model: CompletionModel; } -export class SuggestWidget implements IContentWidget, IDelegate, IDisposable { +export class SuggestWidget implements IContentWidget, IVirtualDelegate, IDisposable { private static readonly ID: string = 'editor.widget.suggestWidget'; @@ -361,7 +368,7 @@ export class SuggestWidget implements IContentWidget, IDelegate private state: State; private isAuto: boolean; private loadingTimeout: number; - private currentSuggestionDetails: TPromise; + private currentSuggestionDetails: CancelablePromise; private focusedItem: ICompletionItem; private ignoreFocusEvents = false; private completionModel: CompletionModel; @@ -377,8 +384,8 @@ export class SuggestWidget implements IContentWidget, IDelegate private suggestWidgetMultipleSuggestions: IContextKey; private suggestionSupportsAutoAccept: IContextKey; - private editorBlurTimeout: TPromise; - private showTimeout: TPromise; + private readonly editorBlurTimeout = new TimeoutTimer(); + private readonly showTimeout = new TimeoutTimer(); private toDispose: IDisposable[]; private onDidSelectEmitter = new Emitter(); @@ -401,6 +408,8 @@ export class SuggestWidget implements IContentWidget, IDelegate private storageServiceAvailable: boolean = true; private expandSuggestionDocs: boolean = false; + private firstFocusInCurrentList: boolean = false; + constructor( private editor: ICodeEditor, @ITelemetryService private telemetryService: ITelemetryService, @@ -450,7 +459,6 @@ export class SuggestWidget implements IContentWidget, IDelegate listInactiveFocusOutline: activeContrastBorder }), themeService.onThemeChange(t => this.onThemeChange(t)), - editor.onDidBlurEditorText(() => this.onEditorBlur()), editor.onDidLayoutChange(() => this.onEditorLayoutChange()), this.list.onSelectionChange(e => this.onListSelection(e)), this.list.onFocusChange(e => this.onListFocus(e)), @@ -475,18 +483,6 @@ export class SuggestWidget implements IContentWidget, IDelegate this.editor.layoutContentWidget(this); } - private onEditorBlur(): void { - if (sticky) { - return; - } - - this.editorBlurTimeout = TPromise.timeout(150).then(() => { - if (!this.editor.hasTextFocus()) { - this.setState(State.Hidden); - } - }); - } - private onEditorLayoutChange(): void { if ((this.state === State.Open || this.state === State.Details) && this.expandDocsSettingFromStorage()) { this.expandSideOrBelow(); @@ -500,7 +496,7 @@ export class SuggestWidget implements IContentWidget, IDelegate const item = e.elements[0]; const index = e.indexes[0]; - item.resolve().then(() => { + item.resolve(CancellationToken.None).then(() => { this.onDidSelectEmitter.fire({ item, index, model: this.completionModel }); alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label)); this.editor.focus(); @@ -566,6 +562,7 @@ export class SuggestWidget implements IContentWidget, IDelegate const item = e.elements[0]; this._ariaAlert(this._getSuggestionAriaAlertLabel(item)); + this.firstFocusInCurrentList = !this.focusedItem; if (item === this.focusedItem) { return; } @@ -583,22 +580,29 @@ export class SuggestWidget implements IContentWidget, IDelegate this.list.reveal(index); - this.currentSuggestionDetails = item.resolve() - .then(() => { - // item can have extra information, so re-render - this.ignoreFocusEvents = true; - this.list.splice(index, 1, [item]); - this.list.setFocus([index]); - this.ignoreFocusEvents = false; + this.currentSuggestionDetails = createCancelablePromise(token => item.resolve(token)); - if (this.expandDocsSettingFromStorage()) { - this.showDetails(); - } else { - removeClass(this.element, 'docs-side'); - } - }) - .then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err)) - .then(() => this.currentSuggestionDetails = null); + this.currentSuggestionDetails.then(() => { + if (this.list.length < index) { + return; + } + + // item can have extra information, so re-render + this.ignoreFocusEvents = true; + this.list.splice(index, 1, [item]); + this.list.setFocus([index]); + this.ignoreFocusEvents = false; + + if (this.expandDocsSettingFromStorage()) { + this.showDetails(); + } else { + removeClass(this.element, 'docs-side'); + } + }).catch(onUnexpectedError).then(() => { + if (this.focusedItem === item) { + this.currentSuggestionDetails = null; + } + }); // emit an event this.onDidFocusEmitter.fire({ item, index, model: this.completionModel }); @@ -622,6 +626,7 @@ export class SuggestWidget implements IContentWidget, IDelegate if (stateChanged) { this.list.splice(0, this.list.length); } + this.focusedItem = null; break; case State.Loading: this.messageElement.textContent = SuggestWidget.LOADING_MESSAGE; @@ -629,6 +634,7 @@ export class SuggestWidget implements IContentWidget, IDelegate show(this.messageElement); removeClass(this.element, 'docs-side'); this.show(); + this.focusedItem = null; break; case State.Empty: this.messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE; @@ -636,6 +642,7 @@ export class SuggestWidget implements IContentWidget, IDelegate show(this.messageElement); removeClass(this.element, 'docs-side'); this.show(); + this.focusedItem = null; break; case State.Open: hide(this.messageElement); @@ -679,7 +686,6 @@ export class SuggestWidget implements IContentWidget, IDelegate if (this.completionModel !== completionModel) { this.completionModel = completionModel; - this.focusedItem = null; } if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) { @@ -702,18 +708,21 @@ export class SuggestWidget implements IContentWidget, IDelegate this.completionModel = null; } else { - const { stats } = this.completionModel; - stats['wasAutomaticallyTriggered'] = !!isAuto; - /* __GDPR__ - "suggestWidget" : { - "wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${ICompletionStats}", - "${EditorTelemetryData}" - ] - } - */ - this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() }); + + if (this.state !== State.Open) { + const { stats } = this.completionModel; + stats['wasAutomaticallyTriggered'] = !!isAuto; + /* __GDPR__ + "suggestWidget" : { + "wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${ICompletionStats}", + "${EditorTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() }); + } this.list.splice(0, this.list.length, this.completionModel.items); @@ -873,7 +882,7 @@ export class SuggestWidget implements IContentWidget, IDelegate */ this.telemetryService.publicLog('suggestWidget:collapseDetails', this.editor.getTelemetryData()); } else { - if (this.state !== State.Open && this.state !== State.Details) { + if (this.state !== State.Open && this.state !== State.Details && this.state !== State.Frozen) { return; } @@ -920,10 +929,10 @@ export class SuggestWidget implements IContentWidget, IDelegate this.suggestWidgetVisible.set(true); - this.showTimeout = TPromise.timeout(100).then(() => { + this.showTimeout.cancelAndSet(() => { addClass(this.element, 'visible'); this.onDidShowEmitter.fire(this); - }); + }, 100); } private hide(): void { @@ -1004,11 +1013,17 @@ export class SuggestWidget implements IContentWidget, IDelegate } private expandSideOrBelow() { + if (!canExpandCompletionItem(this.focusedItem) && this.firstFocusInCurrentList) { + removeClass(this.element, 'docs-side'); + removeClass(this.element, 'docs-below'); + return; + } + let matches = this.element.style.maxWidth.match(/(\d+)px/); if (!matches || Number(matches[1]) < this.maxWidgetWidth) { addClass(this.element, 'docs-below'); removeClass(this.element, 'docs-side'); - } else { + } else if (canExpandCompletionItem(this.focusedItem)) { addClass(this.element, 'docs-side'); removeClass(this.element, 'docs-below'); } @@ -1071,15 +1086,8 @@ export class SuggestWidget implements IContentWidget, IDelegate this.loadingTimeout = null; } - if (this.editorBlurTimeout) { - this.editorBlurTimeout.cancel(); - this.editorBlurTimeout = null; - } - - if (this.showTimeout) { - this.showTimeout.cancel(); - this.showTimeout = null; - } + this.editorBlurTimeout.dispose(); + this.showTimeout.dispose(); } } diff --git a/src/vs/editor/contrib/suggest/test/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/completionModel.test.ts index 87631a1720a..bcb1a675914 100644 --- a/src/vs/editor/contrib/suggest/test/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/completionModel.test.ts @@ -167,7 +167,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, 'top'); + }, { snippets: 'top', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; @@ -186,7 +186,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, 'bottom'); + }, { snippets: 'bottom', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; @@ -204,7 +204,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: 's', characterCountDelta: 0 - }, 'inline'); + }, { snippets: 'inline', snippetsPreventQuickSuggestions: true, filterGraceful: true }); assert.equal(model.items.length, 2); const [a, b] = model.items; @@ -267,7 +267,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: '', characterCountDelta: 0 - }, 'inline'); + }); assert.equal(model.items.length, 5); @@ -294,7 +294,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: '', characterCountDelta: 0 - }, 'inline'); + }); // query gets longer, narrow down the narrow-down'ed-set from before model.lineContext = { leadingLineContent: 'rlut', characterCountDelta: 4 }; @@ -316,7 +316,7 @@ suite('CompletionModel', function () { ], 1, { leadingLineContent: '', characterCountDelta: 0 - }, 'inline'); + }); model.lineContext = { leadingLineContent: 'form', characterCountDelta: 4 }; assert.equal(model.items.length, 5); diff --git a/src/vs/editor/contrib/suggest/test/suggest.test.ts b/src/vs/editor/contrib/suggest/test/suggest.test.ts index 0ee9c8d5715..f8f5d2133ff 100644 --- a/src/vs/editor/contrib/suggest/test/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggest.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { SuggestRegistry } from 'vs/editor/common/modes'; import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest'; diff --git a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts index 4025fcd221e..02509927e62 100644 --- a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import { LRUMemory, NoMemory, PrefixMemory } from 'vs/editor/contrib/suggest/suggestMemory'; +import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory'; import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ICompletionItem } from 'vs/editor/contrib/suggest/completionModel'; @@ -28,6 +28,46 @@ suite('SuggestMemories', function () { ]; }); + test('AbstractMemory, select', function () { + + const mem = new class extends Memory { + memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void { + throw new Error('Method not implemented.'); + } toJSON(): object { + throw new Error('Method not implemented.'); + } + fromJSON(data: object): void { + throw new Error('Method not implemented.'); + } + }; + + let item1 = createSuggestItem('fazz', 0); + let item2 = createSuggestItem('bazz', 0); + let item3 = createSuggestItem('bazz', 0); + let item4 = createSuggestItem('bazz', 0); + item1.suggestion.preselect = false; + item2.suggestion.preselect = true; + item3.suggestion.preselect = true; + + assert.equal(mem.select(buffer, pos, [item1, item2, item3, item4]), 1); + }); + + test('[No|Prefix|LRU]Memory honor selection boost', function () { + let item1 = createSuggestItem('fazz', 0); + let item2 = createSuggestItem('bazz', 0); + let item3 = createSuggestItem('bazz', 0); + let item4 = createSuggestItem('bazz', 0); + item1.suggestion.preselect = false; + item2.suggestion.preselect = true; + item3.suggestion.preselect = true; + let items = [item1, item2, item3, item4]; + + + assert.equal(new NoMemory().select(buffer, pos, items), 1); + assert.equal(new LRUMemory().select(buffer, pos, items), 1); + assert.equal(new PrefixMemory().select(buffer, pos, items), 1); + }); + test('NoMemory', function () { const mem = new NoMemory(); diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 69a6c6870e0..56691b842dc 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; @@ -30,13 +30,15 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; function createMockEditor(model: TextModel): TestCodeEditor { - return createTestCodeEditor({ + let editor = createTestCodeEditor({ model: model, serviceCollection: new ServiceCollection( [ITelemetryService, NullTelemetryService], [IStorageService, NullStorageService] - ) + ), }); + editor.registerAndInstantiateContribution(SnippetController2); + return editor; } suite('SuggestModel - Context', function () { diff --git a/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts b/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts index e3aa3349af1..87cb996bb1c 100644 --- a/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts +++ b/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts @@ -9,6 +9,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class ToggleTabFocusModeAction extends EditorAction { @@ -23,7 +24,8 @@ export class ToggleTabFocusModeAction extends EditorAction { kbOpts: { kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_M, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_M } + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_M }, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts index c639a47bb00..dbf375aa46c 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -2,19 +2,18 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as nls from 'vs/nls'; -import { sequence, asWinJsPromise } from 'vs/base/common/async'; -import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { first2, createCancelablePromise, CancelablePromise, timeout } from 'vs/base/common/async'; +import { onUnexpectedExternalError, onUnexpectedError } from 'vs/base/common/errors'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { registerEditorContribution, EditorAction, IActionOptions, registerEditorAction, registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProviderRegistry } from 'vs/editor/common/modes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; import { registerColor, editorSelectionHighlight, overviewRulerSelectionHighlightForeground, activeContrastBorder, editorSelectionHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -23,9 +22,11 @@ import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/cont import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { firstIndex } from 'vs/base/common/arrays'; +import { firstIndex, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, TrackedRangeStickiness, OverviewRulerLane, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export const editorWordHighlight = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hc: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque to not hide underlying decorations.'), true); export const editorWordHighlightStrong = registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hc: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque to not hide underlying decorations.'), true); @@ -37,50 +38,137 @@ export const overviewRulerWordHighlightStrongForeground = registerColor('editorO export const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); -export function getOccurrencesAtPosition(model: ITextModel, position: Position): TPromise { +export function getOccurrencesAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise { const orderedByScore = DocumentHighlightProviderRegistry.ordered(model); - let foundResult = false; // in order of score ask the occurrences provider // until someone response with a good result // (good = none empty array) - return sequence(orderedByScore.map(provider => { - return (): TPromise => { - if (!foundResult) { - return asWinJsPromise((token) => { - return provider.provideDocumentHighlights(model, position, token); - }).then(data => { - if (Array.isArray(data) && data.length > 0) { - foundResult = true; - return data; - } - return undefined; - }, err => { - onUnexpectedExternalError(err); - return undefined; - }); - } - return undefined; - }; - })).then(values => { - return values[0]; - }); + return first2(orderedByScore.map(provider => () => { + return Promise.resolve(provider.provideDocumentHighlights(model, position, token)) + .then(undefined, onUnexpectedExternalError); + }), result => !isFalsyOrEmpty(result)); } -registerDefaultLanguageCommand('_executeDocumentHighlights', getOccurrencesAtPosition); +interface IOccurenceAtPositionRequest { + readonly result: Promise; + isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean; + cancel(): void; +} + +abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest { + + private readonly _wordRange: Range; + public readonly result: CancelablePromise; + + constructor(model: ITextModel, selection: Selection, wordSeparators: string) { + this._wordRange = this._getCurrentWordRange(model, selection); + this.result = createCancelablePromise(token => this._compute(model, selection, wordSeparators, token)); + } + + protected abstract _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Thenable; + + private _getCurrentWordRange(model: ITextModel, selection: Selection): Range { + const word = model.getWordAtPosition(selection.getPosition()); + if (word) { + return new Range(selection.startLineNumber, word.startColumn, selection.startLineNumber, word.endColumn); + } + return null; + } + + public isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean { + + const lineNumber = selection.startLineNumber; + const startColumn = selection.startColumn; + const endColumn = selection.endColumn; + const currentWordRange = this._getCurrentWordRange(model, selection); + + let requestIsValid = (this._wordRange && this._wordRange.equalsRange(currentWordRange)); + + // Even if we are on a different word, if that word is in the decorations ranges, the request is still valid + // (Same symbol) + for (let i = 0, len = decorationIds.length; !requestIsValid && i < len; i++) { + let range = model.getDecorationRange(decorationIds[i]); + if (range && range.startLineNumber === lineNumber) { + if (range.startColumn <= startColumn && range.endColumn >= endColumn) { + requestIsValid = true; + } + } + } + + return requestIsValid; + } + + public cancel(): void { + this.result.cancel(); + } +} + +class SemanticOccurenceAtPositionRequest extends OccurenceAtPositionRequest { + protected _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Thenable { + return getOccurrencesAtPosition(model, selection.getPosition(), token); + } +} + +class TextualOccurenceAtPositionRequest extends OccurenceAtPositionRequest { + + private _selectionIsEmpty: boolean; + + constructor(model: ITextModel, selection: Selection, wordSeparators: string) { + super(model, selection, wordSeparators); + this._selectionIsEmpty = selection.isEmpty(); + } + + protected _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Thenable { + return timeout(250, token).then(() => { + if (!selection.isEmpty()) { + return []; + } + + const word = model.getWordAtPosition(selection.getPosition()); + + if (!word) { + return []; + } + const matches = model.findMatches(word.word, true, false, true, wordSeparators, false); + return matches.map(m => { + return { + range: m.range, + kind: DocumentHighlightKind.Text + }; + }); + }); + } + + public isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean { + const currentSelectionIsEmpty = selection.isEmpty(); + if (this._selectionIsEmpty !== currentSelectionIsEmpty) { + return false; + } + return super.isValid(model, selection, decorationIds); + } +} + +function computeOccurencesAtPosition(model: ITextModel, selection: Selection, wordSeparators: string): IOccurenceAtPositionRequest { + if (DocumentHighlightProviderRegistry.has(model)) { + return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators); + } + return new TextualOccurenceAtPositionRequest(model, selection, wordSeparators); +} + +registerDefaultLanguageCommand('_executeDocumentHighlights', (model, position) => getOccurrencesAtPosition(model, position, CancellationToken.None)); class WordHighlighter { private editor: ICodeEditor; private occurrencesHighlight: boolean; private model: ITextModel; - private _lastWordRange: Range; private _decorationIds: string[]; private toUnhook: IDisposable[]; private workerRequestTokenId: number = 0; - private workerRequest: TPromise = null; + private workerRequest: IOccurenceAtPositionRequest; private workerRequestCompleted: boolean = false; private workerRequestValue: DocumentHighlight[] = []; @@ -127,7 +215,6 @@ class WordHighlighter { } })); - this._lastWordRange = null; this._decorationIds = []; this.workerRequestTokenId = 0; this.workerRequest = null; @@ -191,8 +278,6 @@ class WordHighlighter { } private _stopAll(): void { - this._lastWordRange = null; - // Remove any existing decorations this._removeDecorations(); @@ -233,12 +318,6 @@ class WordHighlighter { } private _run(): void { - // no providers for this model - if (!DocumentHighlightProviderRegistry.has(this.model)) { - this._stopAll(); - return; - } - let editorSelection = this.editor.getSelection(); // ignore multiline selection @@ -267,21 +346,7 @@ class WordHighlighter { // - 250ms later after the last cursor move event, render the occurrences // - no flickering! - let currentWordRange = new Range(lineNumber, word.startColumn, lineNumber, word.endColumn); - - let workerRequestIsValid = this._lastWordRange && this._lastWordRange.equalsRange(currentWordRange); - - // Even if we are on a different word, if that word is in the decorations ranges, the request is still valid - // (Same symbol) - for (let i = 0, len = this._decorationIds.length; !workerRequestIsValid && i < len; i++) { - let range = this.model.getDecorationRange(this._decorationIds[i]); - if (range && range.startLineNumber === lineNumber) { - if (range.startColumn <= startColumn && range.endColumn >= endColumn) { - workerRequestIsValid = true; - } - } - } - + const workerRequestIsValid = (this.workerRequest && this.workerRequest.isValid(this.model, editorSelection, this._decorationIds)); // There are 4 cases: // a) old workerRequest is valid & completed, renderDecorationsTimer fired @@ -310,18 +375,16 @@ class WordHighlighter { let myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; - this.workerRequest = getOccurrencesAtPosition(this.model, this.editor.getPosition()); + this.workerRequest = computeOccurencesAtPosition(this.model, this.editor.getSelection(), this.editor.getConfiguration().wordSeparators); - this.workerRequest.then(data => { + this.workerRequest.result.then(data => { if (myRequestId === this.workerRequestTokenId) { this.workerRequestCompleted = true; this.workerRequestValue = data || []; this._beginRenderDecorations(); } - }).done(); + }, onUnexpectedError); } - - this._lastWordRange = currentWordRange; } private _beginRenderDecorations(): void { @@ -478,7 +541,8 @@ class NextWordHighlightAction extends WordHighlightNavigationAction { precondition: ctxHasWordHighlights, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyCode.F7 + primary: KeyCode.F7, + weight: KeybindingWeight.EditorContrib } }); } @@ -493,7 +557,8 @@ class PrevWordHighlightAction extends WordHighlightNavigationAction { precondition: ctxHasWordHighlights, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyCode.F7 + primary: KeyMod.Shift | KeyCode.F7, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index bd34ee01bda..eb3361d3ef3 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -18,6 +18,7 @@ import { } from 'vs/editor/contrib/wordOperations/wordOperations'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { deserializePipePositions, testRepeatedActionAndExtractPositions, serializePipePositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; suite('WordOperations', () => { @@ -43,16 +44,16 @@ suite('WordOperations', () => { function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void { command.runEditorCommand(null, editor, null); } - function moveWordLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { + function cursorWordLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { runEditorCommand(editor, inSelectionMode ? _cursorWordLeftSelect : _cursorWordLeft); } - function moveWordStartLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { + function cursorWordStartLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { runEditorCommand(editor, inSelectionMode ? _cursorWordStartLeftSelect : _cursorWordStartLeft); } - function moveWordEndLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { + function cursorWordEndLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { runEditorCommand(editor, inSelectionMode ? _cursorWordEndLeftSelect : _cursorWordEndLeft); } - function moveWordRight(editor: ICodeEditor, inSelectionMode: boolean = false): void { + function cursorWordRight(editor: ICodeEditor, inSelectionMode: boolean = false): void { runEditorCommand(editor, inSelectionMode ? _cursorWordRightSelect : _cursorWordRight); } function moveWordEndRight(editor: ICodeEditor, inSelectionMode: boolean = false): void { @@ -80,44 +81,27 @@ suite('WordOperations', () => { runEditorCommand(editor, _deleteWordEndRight); } - test('move word left', () => { - withTestCodeEditor([ - ' \tMy First Line\t ', - '\tMy Second Line', - ' Third Line🐶', - '', - '1', - ], {}, (editor, _) => { - editor.setPosition(new Position(5, 2)); - const expectedStops = [ - [5, 1], - [4, 1], - [3, 11], - [3, 5], - [3, 1], - [2, 12], - [2, 5], - [2, 2], - [2, 1], - [1, 15], - [1, 9], - [1, 6], - [1, 1], - [1, 1], - ]; - - let actualStops: number[][] = []; - for (let i = 0; i < expectedStops.length; i++) { - moveWordLeft(editor); - const pos = editor.getPosition(); - actualStops.push([pos.lineNumber, pos.column]); - } - - assert.deepEqual(actualStops, expectedStops); - }); + test('cursorWordLeft - simple', () => { + const EXPECTED = [ + '| \t|My |First |Line\t ', + '|\t|My |Second |Line', + '| |Third |Line🐶', + '|', + '|1', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('move word left selection', () => { + test('cursorWordLeft - with selection', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -126,83 +110,106 @@ suite('WordOperations', () => { '1', ], {}, (editor, _) => { editor.setPosition(new Position(5, 2)); - moveWordLeft(editor, true); + cursorWordLeft(editor, true); assert.deepEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); }); }); - test('issue #832: moveWordLeft', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 50)); - - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 '.length + 1, '001'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + '.length + 1, '002'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 '.length + 1, '003'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '004'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '006'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 '.length + 1, '007'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= '.length + 1, '008'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '009'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text '.length + 1, '010'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more '.length + 1, '011'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some '.length + 1, '012'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* Just '.length + 1, '013'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' /* '.length + 1, '014'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ' '.length + 1, '015'); - }); + test('cursorWordLeft - issue #832', () => { + const EXPECTED = ['| |/* |Just |some |more |text |a|+= |3 |+|5-|3 |+ |7 |*/ '].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('moveWordStartLeft', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 50)); - - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 '.length + 1, '001'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + '.length + 1, '002'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 '.length + 1, '003'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '004'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '006'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 '.length + 1, '007'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= '.length + 1, '008'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '009'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text '.length + 1, '010'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more '.length + 1, '011'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some '.length + 1, '012'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* Just '.length + 1, '013'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' /* '.length + 1, '014'); - moveWordStartLeft(editor); assert.equal(editor.getPosition().column, ' '.length + 1, '015'); - }); + test('cursorWordLeft - issue #48046: Word selection doesn\'t work as usual', () => { + const EXPECTED = [ + '|deep.|object.|property', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 21), + ed => cursorWordLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('moveWordEndLeft', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 50)); - - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */'.length + 1, '001'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7'.length + 1, '002'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 +'.length + 1, '003'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3'.length + 1, '004'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '005'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '006'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '007'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3'.length + 1, '008'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+='.length + 1, '009'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '010'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more text'.length + 1, '011'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some more'.length + 1, '012'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just some'.length + 1, '013'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /* Just'.length + 1, '014'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ' /*'.length + 1, '015'); - moveWordEndLeft(editor); assert.equal(editor.getPosition().column, ''.length + 1, '016'); - }); + test('cursorWordStartLeft', () => { + // This is the behaviour observed in Visual Studio, please do not touch test + const EXPECTED = ['| |/* |Just |some |more |text |a|+= |3 |+|5|-|3 |+ |7 |*/| '].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordStartLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('move word right', () => { + test('cursorWordStartLeft - issue #51119: regression makes VS compatibility impossible', () => { + // This is the behaviour observed in Visual Studio, please do not touch test + const EXPECTED = ['|this|.|is|.|a|.|test'].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordStartLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('cursorWordEndLeft', () => { + const EXPECTED = ['| /*| Just| some| more| text| a|+=| 3| +|5|-|3| +| 7| */| '].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => cursorWordEndLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('cursorWordRight - simple', () => { + const EXPECTED = [ + ' \tMy| First| Line|\t |', + '\tMy| Second| Line|', + ' Third| Line🐶|', + '|', + '1|', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(5, 2)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('cursorWordRight - selection', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -211,145 +218,92 @@ suite('WordOperations', () => { '1', ], {}, (editor, _) => { editor.setPosition(new Position(1, 1)); - let expectedStops = [ - [1, 8], - [1, 14], - [1, 19], - [1, 21], - [2, 4], - [2, 11], - [2, 16], - [3, 10], - [3, 17], - [4, 1], - [5, 2], - [5, 2], - ]; - - let actualStops: number[][] = []; - for (let i = 0; i < expectedStops.length; i++) { - moveWordRight(editor); - let pos = editor.getPosition(); - actualStops.push([pos.lineNumber, pos.column]); - } - - assert.deepEqual(actualStops, expectedStops); - }); - }); - - test('move word right selection', () => { - withTestCodeEditor([ - ' \tMy First Line\t ', - '\tMy Second Line', - ' Third Line🐶', - '', - '1', - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 1)); - moveWordRight(editor, true); + cursorWordRight(editor, true); assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); }); }); - test('issue #832: moveWordRight', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 1)); - - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /*'.length + 1, '001'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just'.length + 1, '003'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some'.length + 1, '004'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more'.length + 1, '005'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text'.length + 1, '006'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '007'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+='.length + 1, '008'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3'.length + 1, '009'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '011'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3'.length + 1, '013'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 +'.length + 1, '014'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7'.length + 1, '015'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */'.length + 1, '016'); - moveWordRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */ '.length + 1, '016'); - - }); + test('cursorWordRight - issue #832', () => { + const EXPECTED = [ + ' /*| Just| some| more| text| a|+=| 3| +5|-3| +| 7| */| |', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 50)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('issue #41199: moveWordRight', () => { - withTestCodeEditor([ - 'console.log(err)' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 1)); - - moveWordRight(editor); assert.equal(editor.getPosition().column, 'console'.length + 1, '001'); - moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log'.length + 1, '002'); - moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log(err'.length + 1, '003'); - moveWordRight(editor); assert.equal(editor.getPosition().column, 'console.log(err)'.length + 1, '004'); - }); - }); - - test('issue #48046: Word selection doesn\'t work as usual', () => { - withTestCodeEditor([ - 'deep.object.property' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 21)); - - moveWordLeft(editor); assert.equal(editor.getPosition().column, 'deep.object.'.length + 1, '001'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, 'deep.'.length + 1, '002'); - moveWordLeft(editor); assert.equal(editor.getPosition().column, ''.length + 1, '003'); - }); + test('cursorWordRight - issue #41199', () => { + const EXPECTED = [ + 'console|.log|(err|)|', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 17)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); test('moveWordEndRight', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 1)); - - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /*'.length + 1, '001'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just'.length + 1, '003'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some'.length + 1, '004'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more'.length + 1, '005'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text'.length + 1, '006'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '007'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+='.length + 1, '008'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3'.length + 1, '009'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '011'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3'.length + 1, '013'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 +'.length + 1, '014'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7'.length + 1, '015'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */'.length + 1, '016'); - moveWordEndRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */ '.length + 1, '016'); - - }); + const EXPECTED = [ + ' /*| Just| some| more| text| a|+=| 3| +5|-3| +| 7| */| |', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordEndRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 50)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); test('moveWordStartRight', () => { - withTestCodeEditor([ - ' /* Just some more text a+= 3 +5-3 + 7 */ ' - ], {}, (editor, _) => { - editor.setPosition(new Position(1, 1)); - - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' '.length + 1, '001'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* '.length + 1, '002'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just '.length + 1, '003'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some '.length + 1, '004'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more '.length + 1, '005'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text '.length + 1, '006'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a'.length + 1, '007'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= '.length + 1, '008'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 '.length + 1, '009'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +'.length + 1, '010'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5'.length + 1, '011'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-'.length + 1, '012'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 '.length + 1, '013'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + '.length + 1, '014'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 '.length + 1, '015'); - moveWordStartRight(editor); assert.equal(editor.getPosition().column, ' /* Just some more text a+= 3 +5-3 + 7 */ '.length + 1, '016'); - }); + // This is the behaviour observed in Visual Studio, please do not touch test + const EXPECTED = [ + ' |/* |Just |some |more |text |a|+= |3 |+|5|-|3 |+ |7 |*/ |', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordStartRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 50)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('delete word left for non-empty selection', () => { + test('issue #51119: cursorWordStartRight regression makes VS compatibility impossible', () => { + // This is the behaviour observed in Visual Studio, please do not touch test + const EXPECTED = ['this|.|is|.|a|.|test|'].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordStartRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 15)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('deleteWordLeft for non-empty selection', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -365,7 +319,7 @@ suite('WordOperations', () => { }); }); - test('delete word left for caret at beginning of document', () => { + test('deleteWordLeft for cursor at beginning of document', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -381,7 +335,7 @@ suite('WordOperations', () => { }); }); - test('delete word left for caret at end of whitespace', () => { + test('deleteWordLeft for cursor at end of whitespace', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -397,7 +351,7 @@ suite('WordOperations', () => { }); }); - test('delete word left for caret just behind a word', () => { + test('deleteWordLeft for cursor just behind a word', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -413,7 +367,7 @@ suite('WordOperations', () => { }); }); - test('delete word left for caret inside of a word', () => { + test('deleteWordLeft for cursor inside of a word', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -429,7 +383,7 @@ suite('WordOperations', () => { }); }); - test('delete word right for non-empty selection', () => { + test('deleteWordRight for non-empty selection', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -445,7 +399,7 @@ suite('WordOperations', () => { }); }); - test('delete word right for caret at end of document', () => { + test('deleteWordRight for cursor at end of document', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -461,7 +415,7 @@ suite('WordOperations', () => { }); }); - test('delete word right for caret at beggining of whitespace', () => { + test('deleteWordRight for cursor at beggining of whitespace', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -477,7 +431,7 @@ suite('WordOperations', () => { }); }); - test('delete word right for caret just before a word', () => { + test('deleteWordRight for cursor just before a word', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -493,7 +447,7 @@ suite('WordOperations', () => { }); }); - test('delete word right for caret inside of a word', () => { + test('deleteWordRight for cursor inside of a word', () => { withTestCodeEditor([ ' \tMy First Line\t ', '\tMy Second Line', @@ -509,69 +463,55 @@ suite('WordOperations', () => { }); }); - test('issue #832: deleteWordLeft', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 37)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5 */', '001'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5 ', '002'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +', '003'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 ', '004'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= ', '005'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a', '006'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text ', '007'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some ', '008'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* Just ', '009'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' /* ', '010'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), ' ', '011'); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), '', '012'); - }); + test('deleteWordLeft - issue #832', () => { + const EXPECTED = [ + '| |/* |Just |some |text |a|+= |3 |+|5 |*/| ', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 10000), + ed => deleteWordLeft(ed), + ed => ed.getPosition(), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); test('deleteWordStartLeft', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 37)); - - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5 ', '001'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +', '002'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 ', '003'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= ', '004'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a', '005'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text ', '006'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some ', '007'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just ', '008'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' /* ', '009'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), ' ', '010'); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), '', '011'); - }); + const EXPECTED = [ + '| |/* |Just |some |text |a|+= |3 |+|5 |*/ ', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 10000), + ed => deleteWordStartLeft(ed), + ed => ed.getPosition(), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); test('deleteWordEndLeft', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 37)); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5 */', '001'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5', '002'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +', '003'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3', '004'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+=', '005'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a', '006'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text', '007'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some', '008'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /* Just', '009'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), ' /*', '010'); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), '', '011'); - }); + const EXPECTED = [ + '| /*| Just| some| text| a|+=| 3| +|5| */| ', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 10000), + ed => deleteWordEndLeft(ed), + ed => ed.getPosition(), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('issue #24947', () => { + test('deleteWordLeft - issue #24947', () => { withTestCodeEditor([ '{', '}' @@ -600,29 +540,21 @@ suite('WordOperations', () => { }); }); - test('issue #832: deleteWordRight', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5-3 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 1)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), '/* Just some text a+= 3 +5-3 */ ', '001'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' Just some text a+= 3 +5-3 */ ', '002'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' some text a+= 3 +5-3 */ ', '003'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' text a+= 3 +5-3 */ ', '004'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' a+= 3 +5-3 */ ', '005'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), '+= 3 +5-3 */ ', '006'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' 3 +5-3 */ ', '007'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' +5-3 */ ', '008'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), '5-3 */ ', '009'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), '-3 */ ', '010'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), '3 */ ', '011'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' */ ', '012'); - deleteWordRight(editor); assert.equal(model.getLineContent(1), ' ', '013'); - }); + test('deleteWordRight - issue #832', () => { + const EXPECTED = ' |/*| Just| some| text| a|+=| 3| +|5|-|3| */| |'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => deleteWordRight(ed), + ed => new Position(1, text.length - ed.getValue().length + 1), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('issue #3882: deleteWordRight', () => { + test('deleteWordRight - issue #3882', () => { withTestCodeEditor([ 'public void Add( int x,', ' int y )' @@ -633,7 +565,7 @@ suite('WordOperations', () => { }); }); - test('issue #3882: deleteWordStartRight', () => { + test('deleteWordStartRight - issue #3882', () => { withTestCodeEditor([ 'public void Add( int x,', ' int y )' @@ -644,7 +576,7 @@ suite('WordOperations', () => { }); }); - test('issue #3882: deleteWordEndRight', () => { + test('deleteWordEndRight - issue #3882', () => { withTestCodeEditor([ 'public void Add( int x,', ' int y )' @@ -656,50 +588,34 @@ suite('WordOperations', () => { }); test('deleteWordStartRight', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5-3 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 1)); - - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '/* Just some text a+= 3 +5-3 */ ', '001'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'Just some text a+= 3 +5-3 */ ', '002'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'some text a+= 3 +5-3 */ ', '003'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'text a+= 3 +5-3 */ ', '004'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'a+= 3 +5-3 */ ', '005'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '+= 3 +5-3 */ ', '006'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '3 +5-3 */ ', '007'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '+5-3 */ ', '008'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '5-3 */ ', '009'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '-3 */ ', '010'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '3 */ ', '011'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '*/ ', '012'); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), '', '013'); - }); + const EXPECTED = ' |/* |Just |some |text |a|+= |3 |+|5|-|3 |*/ |'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => deleteWordStartRight(ed), + ed => new Position(1, text.length - ed.getValue().length + 1), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); test('deleteWordEndRight', () => { - withTestCodeEditor([ - ' /* Just some text a+= 3 +5-3 */ ' - ], {}, (editor, _) => { - const model = editor.getModel(); - editor.setPosition(new Position(1, 1)); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' Just some text a+= 3 +5-3 */ ', '001'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' some text a+= 3 +5-3 */ ', '002'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' text a+= 3 +5-3 */ ', '003'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' a+= 3 +5-3 */ ', '004'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), '+= 3 +5-3 */ ', '005'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' 3 +5-3 */ ', '006'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' +5-3 */ ', '007'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), '5-3 */ ', '008'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), '-3 */ ', '009'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), '3 */ ', '010'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' */ ', '011'); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), ' ', '012'); - }); + const EXPECTED = ' /*| Just| some| text| a|+=| 3| +|5|-|3| */| |'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => deleteWordEndRight(ed), + ed => new Position(1, text.length - ed.getValue().length + 1), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); }); - test('issue #3882 (1): Ctrl+Delete removing entire line when used at the end of line', () => { + test('deleteWordRight - issue #3882 (1): Ctrl+Delete removing entire line when used at the end of line', () => { withTestCodeEditor([ 'A line with text.', ' And another one' @@ -710,7 +626,7 @@ suite('WordOperations', () => { }); }); - test('issue #3882 (2): Ctrl+Delete removing entire line when used at the end of line', () => { + test('deleteWordLeft - issue #3882 (2): Ctrl+Delete removing entire line when used at the end of line', () => { withTestCodeEditor([ 'A line with text.', ' And another one' diff --git a/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts b/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts new file mode 100644 index 00000000000..8ab24036b80 --- /dev/null +++ b/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Position } from 'vs/editor/common/core/position'; +import { withTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; + +export function deserializePipePositions(text: string): [string, Position[]] { + let resultText = ''; + let lineNumber = 1; + let charIndex = 0; + let positions: Position[] = []; + for (let i = 0, len = text.length; i < len; i++) { + const chr = text.charAt(i); + if (chr === '\n') { + resultText += chr; + lineNumber++; + charIndex = 0; + continue; + } + if (chr === '|') { + positions.push(new Position(lineNumber, charIndex + 1)); + } else { + resultText += chr; + charIndex++; + } + } + return [resultText, positions]; +} + +export function serializePipePositions(text: string, positions: Position[]): string { + positions.sort(Position.compare); + let resultText = ''; + let lineNumber = 1; + let charIndex = 0; + for (let i = 0, len = text.length; i < len; i++) { + const chr = text.charAt(i); + if (positions.length > 0 && positions[0].lineNumber === lineNumber && positions[0].column === charIndex + 1) { + resultText += '|'; + positions.shift(); + } + resultText += chr; + if (chr === '\n') { + lineNumber++; + charIndex = 0; + } else { + charIndex++; + } + } + if (positions.length > 0 && positions[0].lineNumber === lineNumber && positions[0].column === charIndex + 1) { + resultText += '|'; + positions.shift(); + } + if (positions.length > 0) { + throw new Error(`Unexpected left over positions!!!`); + } + return resultText; +} + +export function testRepeatedActionAndExtractPositions(text: string, initialPosition: Position, action: (editor: TestCodeEditor) => void, record: (editor: TestCodeEditor) => Position, stopCondition: (editor: TestCodeEditor) => boolean): Position[] { + let actualStops: Position[] = []; + withTestCodeEditor(text, {}, (editor, _) => { + editor.setPosition(initialPosition); + while (true) { + action(editor); + actualStops.push(record(editor)); + if (stopCondition(editor)) { + break; + } + + if (actualStops.length > 1000) { + throw new Error(`Endless loop detected!`); + } + } + }); + return actualStops; +} diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 528303ab72c..a46cfb237ab 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -19,6 +19,7 @@ import { getMapForWordSeparators, WordCharacterClassifier } from 'vs/editor/comm import { CursorState } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export interface MoveWordOptions extends ICommandOptions { inSelectionMode: boolean; @@ -100,7 +101,8 @@ export class CursorWordStartLeft extends WordLeftCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyCode.LeftArrow } + mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib } }); } @@ -121,7 +123,7 @@ export class CursorWordLeft extends WordLeftCommand { constructor() { super({ inSelectionMode: false, - wordNavigationType: WordNavigationType.WordStart, + wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeft', precondition: null }); @@ -138,7 +140,8 @@ export class CursorWordStartLeftSelect extends WordLeftCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow } + mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib } }); } @@ -187,7 +190,8 @@ export class CursorWordEndRight extends WordRightCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.RightArrow, - mac: { primary: KeyMod.Alt | KeyCode.RightArrow } + mac: { primary: KeyMod.Alt | KeyCode.RightArrow }, + weight: KeybindingWeight.EditorContrib } }); } @@ -225,7 +229,8 @@ export class CursorWordEndRightSelect extends WordRightCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow, - mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow } + mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow }, + weight: KeybindingWeight.EditorContrib } }); } @@ -330,7 +335,8 @@ export class DeleteWordLeft extends DeleteWordLeftCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Backspace, - mac: { primary: KeyMod.Alt | KeyCode.Backspace } + mac: { primary: KeyMod.Alt | KeyCode.Backspace }, + weight: KeybindingWeight.EditorContrib } }); } @@ -368,7 +374,8 @@ export class DeleteWordRight extends DeleteWordRightCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Delete, - mac: { primary: KeyMod.Alt | KeyCode.Delete } + mac: { primary: KeyMod.Alt | KeyCode.Delete }, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts new file mode 100644 index 00000000000..67864f7df46 --- /dev/null +++ b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { Position } from 'vs/editor/common/core/position'; +import { DeleteWordPartLeft, DeleteWordPartRight, CursorWordPartLeft, CursorWordPartRight } from 'vs/editor/contrib/wordPartOperations/wordPartOperations'; +import { EditorCommand } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { deserializePipePositions, testRepeatedActionAndExtractPositions, serializePipePositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; + +suite('WordPartOperations', () => { + const _deleteWordPartLeft = new DeleteWordPartLeft(); + const _deleteWordPartRight = new DeleteWordPartRight(); + const _cursorWordPartLeft = new CursorWordPartLeft(); + const _cursorWordPartRight = new CursorWordPartRight(); + + function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void { + command.runEditorCommand(null, editor, null); + } + function moveWordPartLeft(editor: ICodeEditor, inSelectionmode: boolean = false): void { + runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartLeft); + } + function moveWordPartRight(editor: ICodeEditor, inSelectionmode: boolean = false): void { + runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartRight); + } + function deleteWordPartLeft(editor: ICodeEditor): void { + runEditorCommand(editor, _deleteWordPartLeft); + } + function deleteWordPartRight(editor: ICodeEditor): void { + runEditorCommand(editor, _deleteWordPartRight); + } + + test('move word part left basic', () => { + const EXPECTED = [ + '|start| |line|', + '|this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|', + '|end| |line' + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => moveWordPartLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('issue #53899: move word part left whitespace', () => { + const EXPECTED = '|myvar| |=| |\'|demonstration| |of| |selection| |with| |space|\''; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => moveWordPartLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('issue #53899: move word part left underscores', () => { + const EXPECTED = '|myvar| |=| |\'|demonstration|_____of| |selection| |with| |space|\''; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1000, 1000), + ed => moveWordPartLeft(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('move word part right basic', () => { + const EXPECTED = [ + 'start| |line|', + '|this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|', + '|end| |line|' + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordPartRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(3, 9)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('issue #53899: move word part right whitespace', () => { + const EXPECTED = 'myvar| =| \'demonstration| |of| |selection| |with| |space|\'|'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordPartRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 52)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('issue #53899: move word part right underscores', () => { + const EXPECTED = 'myvar| =| \'demonstration_____|of| |selection| |with| |space|\'|'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => moveWordPartRight(ed), + ed => ed.getPosition(), + ed => ed.getPosition().equals(new Position(1, 52)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('delete word part left basic', () => { + const EXPECTED = '| |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1000), + ed => deleteWordPartLeft(ed), + ed => ed.getPosition(), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('delete word part right basic', () => { + const EXPECTED = ' |/*| |Just| |some| |text| |a|+=| 3| +|5|-|3| */| |this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|'; + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => deleteWordPartRight(ed), + ed => new Position(1, text.length - ed.getValue().length + 1), + ed => ed.getValue().length === 0 + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); +}); diff --git a/src/vs/editor/contrib/wordPartOperations/wordPartOperations.ts b/src/vs/editor/contrib/wordPartOperations/wordPartOperations.ts new file mode 100644 index 00000000000..fec93549d35 --- /dev/null +++ b/src/vs/editor/contrib/wordPartOperations/wordPartOperations.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ITextModel } from 'vs/editor/common/model'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { Selection } from 'vs/editor/common/core/selection'; +import { registerEditorCommand } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; +import { WordNavigationType, WordPartOperations } from 'vs/editor/common/controller/cursorWordOperations'; +import { WordCharacterClassifier } from 'vs/editor/common/controller/wordCharacterClassifier'; +import { DeleteWordCommand, MoveWordCommand } from '../wordOperations/wordOperations'; +import { Position } from 'vs/editor/common/core/position'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; + +export class DeleteWordPartLeft extends DeleteWordCommand { + constructor() { + super({ + whitespaceHeuristics: true, + wordNavigationType: WordNavigationType.WordStart, + id: 'deleteWordPartLeft', + precondition: EditorContextKeys.writable, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Backspace }, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _delete(wordSeparators: WordCharacterClassifier, model: ITextModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range { + let r = WordPartOperations.deleteWordPartLeft(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType); + if (r) { + return r; + } + return new Range(1, 1, 1, 1); + } +} + +export class DeleteWordPartRight extends DeleteWordCommand { + constructor() { + super({ + whitespaceHeuristics: true, + wordNavigationType: WordNavigationType.WordEnd, + id: 'deleteWordPartRight', + precondition: EditorContextKeys.writable, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Delete }, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _delete(wordSeparators: WordCharacterClassifier, model: ITextModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range { + let r = WordPartOperations.deleteWordPartRight(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType); + if (r) { + return r; + } + const lineCount = model.getLineCount(); + const maxColumn = model.getLineMaxColumn(lineCount); + return new Range(lineCount, maxColumn, lineCount, maxColumn); + } +} + +export class WordPartLeftCommand extends MoveWordCommand { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return WordPartOperations.moveWordPartLeft(wordSeparators, model, position, wordNavigationType); + } +} +export class CursorWordPartLeft extends WordPartLeftCommand { + constructor() { + super({ + inSelectionMode: false, + wordNavigationType: WordNavigationType.WordStart, + id: 'cursorWordPartLeft', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } + }); + } +} +// Register previous id for compatibility purposes +CommandsRegistry.registerCommandAlias('cursorWordPartStartLeft', 'cursorWordPartLeft'); + +export class CursorWordPartLeftSelect extends WordPartLeftCommand { + constructor() { + super({ + inSelectionMode: true, + wordNavigationType: WordNavigationType.WordStart, + id: 'cursorWordPartLeftSelect', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } + }); + } +} +// Register previous id for compatibility purposes +CommandsRegistry.registerCommandAlias('cursorWordPartStartLeftSelect', 'cursorWordPartLeftSelect'); + +export class WordPartRightCommand extends MoveWordCommand { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + return WordPartOperations.moveWordPartRight(wordSeparators, model, position, wordNavigationType); + } +} +export class CursorWordPartRight extends WordPartRightCommand { + constructor() { + super({ + inSelectionMode: false, + wordNavigationType: WordNavigationType.WordEnd, + id: 'cursorWordPartRight', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow }, + weight: KeybindingWeight.EditorContrib + } + }); + } +} +export class CursorWordPartRightSelect extends WordPartRightCommand { + constructor() { + super({ + inSelectionMode: true, + wordNavigationType: WordNavigationType.WordEnd, + id: 'cursorWordPartRightSelect', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow }, + weight: KeybindingWeight.EditorContrib + } + }); + } +} + + +registerEditorCommand(new DeleteWordPartLeft()); +registerEditorCommand(new DeleteWordPartRight()); +registerEditorCommand(new CursorWordPartLeft()); +registerEditorCommand(new CursorWordPartLeftSelect()); +registerEditorCommand(new CursorWordPartRight()); +registerEditorCommand(new CursorWordPartRightSelect()); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index e23a365cca7..cc32769c6a6 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -42,3 +42,4 @@ import 'vs/editor/contrib/suggest/suggestController'; import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; import 'vs/editor/contrib/wordHighlighter/wordHighlighter'; import 'vs/editor/contrib/wordOperations/wordOperations'; +import 'vs/editor/contrib/wordPartOperations/wordPartOperations'; diff --git a/src/vs/editor/editor.api.ts b/src/vs/editor/editor.api.ts index 5594a22627c..7bd274aab60 100644 --- a/src/vs/editor/editor.api.ts +++ b/src/vs/editor/editor.api.ts @@ -35,7 +35,6 @@ export const Position = api.Position; export const Range = api.Range; export const Selection = api.Selection; export const SelectionDirection = api.SelectionDirection; -export const Severity = api.Severity; export const MarkerSeverity = api.MarkerSeverity; export const MarkerTag = api.MarkerTag; export const Promise = api.Promise; diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index 7f6504e4607..d650416ba53 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -26,11 +26,11 @@ import { editorWidgetBackground, widgetShadow, contrastBorder } from 'vs/platfor import * as platform from 'vs/base/common/platform'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; import * as browser from 'vs/base/browser/browser'; import { IEditorConstructionOptions } from 'vs/editor/standalone/browser/standaloneCodeEditor'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey('accessibilityHelpWidgetVisible', false); @@ -342,7 +342,8 @@ class ShowAccessibilityHelpAction extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: (browser.isIE ? KeyMod.CtrlCmd | KeyCode.F1 : KeyMod.Alt | KeyCode.F1) + primary: (browser.isIE ? KeyMod.CtrlCmd | KeyCode.F1 : KeyMod.Alt | KeyCode.F1), + weight: KeybindingWeight.EditorContrib } }); } @@ -366,7 +367,7 @@ registerEditorCommand( precondition: CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE, handler: x => x.hide(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(100), + weight: KeybindingWeight.EditorContrib + 100, kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 298a4d7a0d5..5309a0ebf41 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -14,6 +14,7 @@ import { LineTokens, IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import * as strings from 'vs/base/common/strings'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; +import { TimeoutTimer } from 'vs/base/common/async'; export interface IColorizerOptions { tabSize?: number; @@ -42,26 +43,7 @@ export class Colorizer { let render = (str: string) => { domNode.innerHTML = str; }; - return this.colorize(modeService, text, mimeType, options).then(render, (err) => console.error(err), render); - } - - private static _tokenizationSupportChangedPromise(language: string): TPromise { - let listener: IDisposable = null; - let stopListening = () => { - if (listener) { - listener.dispose(); - listener = null; - } - }; - - return new TPromise((c, e, p) => { - listener = TokenizationRegistry.onDidChange((e) => { - if (e.changedLanguages.indexOf(language) >= 0) { - stopListening(); - c(void 0); - } - }); - }, stopListening); + return this.colorize(modeService, text, mimeType, options).then(render, (err) => console.error(err)); } public static colorize(modeService: IModeService, text: string, mimeType: string, options: IColorizerOptions): TPromise { @@ -84,13 +66,34 @@ export class Colorizer { return TPromise.as(_colorize(lines, options.tabSize, tokenizationSupport)); } - // wait 500ms for mode to load, then give up - return TPromise.any([this._tokenizationSupportChangedPromise(language), TPromise.timeout(500)]).then(_ => { - let tokenizationSupport = TokenizationRegistry.get(language); - if (tokenizationSupport) { - return _colorize(lines, options.tabSize, tokenizationSupport); - } - return _fakeColorize(lines, options.tabSize); + return new TPromise((resolve, reject) => { + let listener: IDisposable = null; + let timeout: TimeoutTimer = null; + + const execute = () => { + if (listener) { + listener.dispose(); + listener = null; + } + if (timeout) { + timeout.dispose(); + timeout = null; + } + const tokenizationSupport = TokenizationRegistry.get(language); + if (tokenizationSupport) { + return resolve(_colorize(lines, options.tabSize, tokenizationSupport)); + } + return resolve(_fakeColorize(lines, options.tabSize)); + }; + + // wait 500ms for mode to load, then give up + timeout = new TimeoutTimer(); + timeout.cancelAndSet(execute, 500); + listener = TokenizationRegistry.onDidChange((e) => { + if (e.changedLanguages.indexOf(language) >= 0) { + execute(); + } + }); }); } @@ -99,6 +102,7 @@ export class Colorizer { const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, mightContainRTL); let renderResult = renderViewLine(new RenderLineInput( false, + true, line, false, isBasicASCII, @@ -152,6 +156,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); let renderResult = renderViewLine(new RenderLineInput( false, + true, line, false, isBasicASCII, @@ -187,6 +192,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); let renderResult = renderViewLine(new RenderLineInput( false, + true, line, false, isBasicASCII, diff --git a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts index 8bd223b6a2f..9076c61eb13 100644 --- a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts +++ b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts @@ -69,7 +69,13 @@ export class QuickOpenController implements editorCommon.IEditorContribution, ID } this.lastKnownEditorSelection = null; - this.editor.focus(); + + // Return focus to the editor if + // - focus is back on the element because no other focusable element was clicked + // - a command was picked from the picker which indicates the editor should get focused + if (document.activeElement === document.body || !canceled) { + this.editor.focus(); + } }; this.widget = new QuickOpenEditorWidget( diff --git a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts index 9f83d8111c9..28bf35f5a56 100644 --- a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts +++ b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts @@ -18,6 +18,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; interface ParseResult { position: Position; @@ -153,7 +154,8 @@ export class GotoLineAction extends BaseEditorQuickOpenAction { kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyMod.CtrlCmd | KeyCode.KEY_G, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G } + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }, + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts b/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts index e44e14c0da8..d5f913ea550 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts @@ -18,6 +18,7 @@ import { registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editor import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as browser from 'vs/base/browser/browser'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class EditorActionCommandEntry extends QuickOpenEntryGroup { private key: string; @@ -49,18 +50,18 @@ export class EditorActionCommandEntry extends QuickOpenEntryGroup { if (mode === Mode.OPEN) { // Use a timeout to give the quick open widget a chance to close itself first - TPromise.timeout(50).done(() => { + setTimeout(() => { // Some actions are enabled only when editor has focus this.editor.focus(); try { let promise = this.action.run() || TPromise.as(null); - promise.done(null, onUnexpectedError); + promise.then(null, onUnexpectedError); } catch (error) { onUnexpectedError(error); } - }, onUnexpectedError); + }, 50); return true; } @@ -79,9 +80,12 @@ export class QuickCommandAction extends BaseEditorQuickOpenAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: (browser.isIE ? KeyMod.Alt | KeyCode.F1 : KeyCode.F1) + primary: (browser.isIE ? KeyMod.Alt | KeyCode.F1 : KeyCode.F1), + weight: KeybindingWeight.EditorContrib }, menuOpts: { + group: 'z_commands', + order: 1 } }); } diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts index a09d76a25d5..83ec34ab012 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts @@ -15,13 +15,15 @@ import { IContext, IHighlight, QuickOpenEntryGroup, QuickOpenModel } from 'vs/ba import { IAutoFocus, Mode } from 'vs/base/parts/quickopen/common/quickOpen'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { SymbolInformation, DocumentSymbolProviderRegistry, symbolKindToCssClass, IOutline, Location } from 'vs/editor/common/modes'; +import { DocumentSymbol, DocumentSymbolProviderRegistry, symbolKindToCssClass } from 'vs/editor/common/modes'; import { BaseEditorQuickOpenAction, IDecorator } from './editorQuickOpen'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; import { registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { CancellationToken } from 'vs/base/common/cancellation'; let SCOPE_PREFIX = ':'; @@ -120,7 +122,8 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { precondition: EditorContextKeys.hasDocumentSymbolProvider, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O, + weight: KeybindingWeight.EditorContrib }, menuOpts: { group: 'navigation', @@ -138,16 +141,16 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { } // Resolve outline - return getDocumentSymbols(model).then((result: IOutline) => { - if (result.entries.length === 0) { + return TPromise.wrap(getDocumentSymbols(model, true, CancellationToken.None).then((result: DocumentSymbol[]) => { + if (result.length === 0) { return; } - this._run(editor, result.entries); - }); + this._run(editor, result); + })); } - private _run(editor: ICodeEditor, result: SymbolInformation[]): void { + private _run(editor: ICodeEditor, result: DocumentSymbol[]): void { this._show(this.getController(editor), { getModel: (value: string): QuickOpenModel => { return new QuickOpenModel(this.toQuickOpenEntries(editor, result, value)); @@ -167,11 +170,11 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { }); } - private symbolEntry(name: string, type: string, description: string, location: Location, highlights: IHighlight[], editor: ICodeEditor, decorator: IDecorator): SymbolEntry { - return new SymbolEntry(name, type, description, Range.lift(location.range), highlights, editor, decorator); + private symbolEntry(name: string, type: string, description: string, range: IRange, highlights: IHighlight[], editor: ICodeEditor, decorator: IDecorator): SymbolEntry { + return new SymbolEntry(name, type, description, Range.lift(range), highlights, editor, decorator); } - private toQuickOpenEntries(editor: ICodeEditor, flattened: SymbolInformation[], searchValue: string): SymbolEntry[] { + private toQuickOpenEntries(editor: ICodeEditor, flattened: DocumentSymbol[], searchValue: string): SymbolEntry[] { const controller = this.getController(editor); let results: SymbolEntry[] = []; @@ -197,7 +200,7 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { } // Add - results.push(this.symbolEntry(label, symbolKindToCssClass(element.kind), description, element.location, highlights, editor, controller)); + results.push(this.symbolEntry(label, symbolKindToCssClass(element.kind), description, element.range, highlights, editor, controller)); } } diff --git a/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts index e0b2289a0ef..3259edf4f88 100644 --- a/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts +++ b/src/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.ts @@ -5,16 +5,12 @@ 'use strict'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; @@ -24,28 +20,20 @@ export class StandaloneReferencesController extends ReferencesController { editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @ICodeEditorService editorService: ICodeEditorService, - @ITextModelService textModelResolverService: ITextModelService, @INotificationService notificationService: INotificationService, @IInstantiationService instantiationService: IInstantiationService, - @IWorkspaceContextService contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @optional(IEnvironmentService) environmentService: IEnvironmentService ) { super( true, editor, contextKeyService, editorService, - textModelResolverService, notificationService, instantiationService, - contextService, storageService, - themeService, configurationService, - environmentService ); } } diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 08f50a00e19..62931784d85 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -5,10 +5,10 @@ 'use strict'; import Severity from 'vs/base/common/severity'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IConfigurationService, IConfigurationChangeEvent, IConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ICommandService, ICommand, ICommandEvent, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IDisposable, IReference, ImmortalReference, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, IReference, ImmortalReference, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeybindingsRegistry, IKeybindingItem } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -45,6 +45,7 @@ import { WorkspaceEdit, isResourceTextEdit, TextEdit } from 'vs/editor/common/mo import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { localize } from 'vs/nls'; +import { ILabelService, LabelRules, RegisterFormatterEvent } from 'vs/platform/label/common/label'; export class SimpleModel implements ITextEditorModel { @@ -68,6 +69,10 @@ export class SimpleModel implements ITextEditorModel { return this.model; } + public isReadonly(): boolean { + return false; + } + public dispose(): void { this._onDispose.fire(); } @@ -230,11 +235,9 @@ export class StandaloneCommandService implements ICommandService { public addCommand(command: ICommand): IDisposable { const { id } = command; this._dynamicCommands[id] = command; - return { - dispose: () => { - delete this._dynamicCommands[id]; - } - }; + return toDisposable(() => { + delete this._dynamicCommands[id]; + }); } public executeCommand(id: string, ...args: any[]): TPromise { @@ -289,18 +292,16 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { weight2: 0 }); - toDispose.push({ - dispose: () => { - for (let i = 0; i < this._dynamicKeybindings.length; i++) { - let kb = this._dynamicKeybindings[i]; - if (kb.command === commandId) { - this._dynamicKeybindings.splice(i, 1); - this.updateResolver({ source: KeybindingSource.Default }); - return; - } + toDispose.push(toDisposable(() => { + for (let i = 0; i < this._dynamicKeybindings.length; i++) { + let kb = this._dynamicKeybindings[i]; + if (kb.command === commandId) { + this._dynamicKeybindings.splice(i, 1); + this.updateResolver({ source: KeybindingSource.Default }); + return; } } - }); + })); let commandService = this._commandService; if (commandService instanceof StandaloneCommandService) { @@ -507,7 +508,7 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { constructor() { const resource = URI.from({ scheme: SimpleWorkspaceContextService.SCHEME, authority: 'model', path: '/' }); - this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: resource, name: '', index: 0 })], name: resource.fsPath }; + this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: resource, name: '', index: 0 })] }; } public getWorkspace(): IWorkspace { @@ -594,3 +595,25 @@ export class SimpleBulkEditService implements IBulkEditService { }); } } + +export class SimpleUriLabelService implements ILabelService { + _serviceBrand: any; + + private readonly _onDidRegisterFormatter: Emitter = new Emitter(); + public readonly onDidRegisterFormatter: Event = this._onDidRegisterFormatter.event; + + public getUriLabel(resource: URI, relative?: boolean): string { + if (resource.scheme === 'file') { + return resource.fsPath; + } + return resource.path; + } + + public getWorkspaceLabel(workspace: IWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { + return ''; + } + + public registerFormatter(schema: string, formatter: LabelRules): IDisposable { + throw new Error('Not implemented'); + } +} diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index c5860572a8e..1396ea01d0a 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -9,14 +9,14 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } -.monaco-menu .monaco-action-bar.vertical .action-item .action-label:focus { +.monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item:focus .action-label { color: #0059AC; stroke-width: 1.2px; text-shadow: 0px 0px 0.15px #0059AC; } -.monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item .action-label:focus, -.monaco-editor.hc-black .monaco-menu .monaco-action-bar.vertical .action-item .action-label:focus { +.monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label, +.monaco-editor.hc-black .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label { color: #ACDDFF; stroke-width: 1.2px; text-shadow: 0px 0px 0.15px #ACDDFF; @@ -211,14 +211,14 @@ } /* contextmenu */ - .monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-item .action-label:focus, - .monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item .action-label:focus { + .monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label, + .monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .action-label { -ms-high-contrast-adjust: none; color: highlighttext !important; background-color: highlight !important; } - .monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-item .action-label:hover, - .monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item .action-label:hover { + .monaco-editor.vs .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .action-label, + .monaco-editor.vs-dark .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .action-label { -ms-high-contrast-adjust: none; background: transparent !important; border: 1px solid highlight; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 22b23704de2..890b51f49bd 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -5,7 +5,7 @@ 'use strict'; -import { empty as emptyDisposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -17,7 +17,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { StandaloneKeybindingService, applyConfigurationValues } from 'vs/editor/standalone/browser/simpleServices'; -import { IEditorContextViewService } from 'vs/editor/standalone/browser/standaloneServices'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -84,7 +84,7 @@ export interface IEditorConstructionOptions extends IEditorOptions { /** * The initial model associated with this code editor. */ - model?: ITextModel; + model?: ITextModel | null; /** * The initial value of the auto created model in the editor. * To not create automatically a model, use `model: null`. @@ -206,7 +206,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon } if (!this._standaloneKeybindingService) { console.warn('Cannot add keybinding because the editor is configured with an unrecognized KeybindingService'); - return emptyDisposable; + return Disposable.None; } // Read descriptor options @@ -272,11 +272,9 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon // Store it under the original id, such that trigger with the original id will work this._actions[id] = internalAction; - toDispose.push({ - dispose: () => { - delete this._actions[id]; - } - }); + toDispose.push(toDisposable(() => { + delete this._actions[id]; + })); return combinedDisposable(toDispose); } @@ -284,7 +282,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon export class StandaloneEditor extends StandaloneCodeEditor implements IStandaloneCodeEditor { - private _contextViewService: IEditorContextViewService; + private _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; private _ownsModel: boolean; @@ -311,7 +309,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon delete options.model; super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, keybindingService, themeService, notificationService); - this._contextViewService = contextViewService; + this._contextViewService = contextViewService; this._configurationService = configurationService; this._register(toDispose); @@ -359,7 +357,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon export class StandaloneDiffEditor extends DiffEditorWidget implements IStandaloneDiffEditor { - private _contextViewService: IEditorContextViewService; + private _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; constructor( @@ -384,7 +382,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super(domElement, options, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService); - this._contextViewService = contextViewService; + this._contextViewService = contextViewService; this._configurationService = configurationService; this._register(toDispose); diff --git a/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts index e9475a0e613..90717c99c68 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeServiceImpl.ts @@ -13,7 +13,7 @@ import { windowOpenNoOpener } from 'vs/base/browser/dom'; import { Schemas } from 'vs/base/common/network'; import { IRange } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export class StandaloneCodeEditorServiceImpl extends CodeEditorServiceImpl { diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 67772c4ff1c..7851d5d20d4 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -11,7 +11,7 @@ import { StandaloneEditor, IStandaloneCodeEditor, StandaloneDiffEditor, IStandal import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IEditorOverrideServices, DynamicStandaloneServices, StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { IDisposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -165,8 +165,8 @@ export function createModel(value: string, language?: string, uri?: URI): ITextM /** * Change the language for a model. */ -export function setModelLanguage(model: ITextModel, language: string): void { - StaticServices.modelService.get().setMode(model, StaticServices.modeService.get().getOrCreateMode(language)); +export function setModelLanguage(model: ITextModel, languageId: string): void { + StaticServices.modelService.get().setMode(model, StaticServices.modeService.get().getOrCreateMode(languageId)); } /** @@ -262,14 +262,14 @@ export function colorizeModelLine(model: ITextModel, lineNumber: number, tabSize /** * @internal */ -function getSafeTokenizationSupport(languageId: string): modes.ITokenizationSupport { - let tokenizationSupport = modes.TokenizationRegistry.get(languageId); +function getSafeTokenizationSupport(language: string): modes.ITokenizationSupport { + let tokenizationSupport = modes.TokenizationRegistry.get(language); if (tokenizationSupport) { return tokenizationSupport; } return { getInitialState: () => NULL_STATE, - tokenize: (line: string, state: modes.IState, deltaOffset: number) => nullTokenize(languageId, line, state, deltaOffset), + tokenize: (line: string, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset), tokenize2: undefined, }; } @@ -297,7 +297,7 @@ export function tokenize(text: string, languageId: string): Token[][] { } /** - * Define a new theme. + * Define a new theme or updte an existing theme. */ export function defineTheme(themeName: string, themeData: IStandaloneThemeData): void { StaticServices.standaloneThemeService.get().defineTheme(themeName, themeData); @@ -394,5 +394,6 @@ export function createMonacoEditorAPI(): typeof monaco.editor { // vars EditorType: editorCommon.EditorType + }; } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 533087ed7b0..f7d7ccd8f40 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -42,6 +42,11 @@ export function getLanguages(): ILanguageExtensionPoint[] { return result; } +export function getEncodedLanguageId(languageId: string): number { + let lid = StaticServices.modeService.get().getLanguageIdentifier(languageId); + return lid && lid.id; +} + /** * An event emitted when a language is first time needed (e.g. a model has it set). * @event @@ -69,6 +74,31 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang return LanguageConfigurationRegistry.register(languageIdentifier, configuration); } +/** + * @internal + */ +export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSupport { + + private readonly _actual: EncodedTokensProvider; + + constructor(actual: EncodedTokensProvider) { + this._actual = actual; + } + + public getInitialState(): modes.IState { + return this._actual.getInitialState(); + } + + public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + throw new Error('Not supported!'); + } + + public tokenize2(line: string, state: modes.IState): TokenizationResult2 { + let result = this._actual.tokenizeEncoded(line, state); + return new TokenizationResult2(result.tokens, result.endState); + } +} + /** * @internal */ @@ -203,6 +233,38 @@ export interface ILineTokens { endState: modes.IState; } +/** + * The result of a line tokenization. + */ +export interface IEncodedLineTokens { + /** + * The tokens on the line in a binary, encoded format. Each token occupies two array indices. For token i: + * - at offset 2*i => startIndex + * - at offset 2*i + 1 => metadata + * Meta data is in binary format: + * - ------------------------------------------- + * 3322 2222 2222 1111 1111 1100 0000 0000 + * 1098 7654 3210 9876 5432 1098 7654 3210 + * - ------------------------------------------- + * bbbb bbbb bfff ffff ffFF FTTT LLLL LLLL + * - ------------------------------------------- + * - L = EncodedLanguageId (8 bits): Use `getEncodedLanguageId` to get the encoded ID of a language. + * - T = StandardTokenType (3 bits): Other = 0, Comment = 1, String = 2, RegEx = 4. + * - F = FontStyle (3 bits): None = 0, Italic = 1, Bold = 2, Underline = 4. + * - f = foreground ColorId (9 bits) + * - b = background ColorId (9 bits) + * - The color value for each colorId is defined in IStandaloneThemeData.customTokenColors: + * e.g colorId = 1 is stored in IStandaloneThemeData.customTokenColors[1]. Color id = 0 means no color, + * id = 1 is for the default foreground color, id = 2 for the default background. + */ + tokens: Uint32Array; + /** + * The tokenization end state. + * A pointer will be held to this and the object should not be modified by the tokenizer after the pointer is returned. + */ + endState: modes.IState; +} + /** * A "manual" provider of tokens. */ @@ -217,18 +279,41 @@ export interface TokensProvider { tokenize(line: string, state: modes.IState): ILineTokens; } +/** + * A "manual" provider of tokens, returning tokens in a binary form. + */ +export interface EncodedTokensProvider { + /** + * The initial state of a language. Will be the state passed in to tokenize the first line. + */ + getInitialState(): modes.IState; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenizeEncoded(line: string, state: modes.IState): IEncodedLineTokens; +} + +function isEncodedTokensProvider(provider: TokensProvider | EncodedTokensProvider): provider is EncodedTokensProvider { + return provider['tokenizeEncoded']; +} /** * Set the tokens provider for a language (manual implementation). */ -export function setTokensProvider(languageId: string, provider: TokensProvider): IDisposable { +export function setTokensProvider(languageId: string, provider: TokensProvider | EncodedTokensProvider): IDisposable { let languageIdentifier = StaticServices.modeService.get().getLanguageIdentifier(languageId); if (!languageIdentifier) { throw new Error(`Cannot set tokens provider for unknown language ${languageId}`); } - let adapter = new TokenizationSupport2Adapter(StaticServices.standaloneThemeService.get(), languageIdentifier, provider); + let adapter: modes.ITokenizationSupport; + if (isEncodedTokensProvider(provider)) { + adapter = new EncodedTokenizationSupport2Adapter(provider); + } else { + adapter = new TokenizationSupport2Adapter(StaticServices.standaloneThemeService.get(), languageIdentifier, provider); + } return modes.TokenizationRegistry.register(languageId, adapter); } + /** * Set the tokens provider for a language (monarch implementation). */ @@ -764,6 +849,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { register: register, getLanguages: getLanguages, onLanguage: onLanguage, + getEncodedLanguageId: getEncodedLanguageId, // provider methods setLanguageConfiguration: setLanguageConfiguration, @@ -794,7 +880,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { SymbolKind: modes.SymbolKind, IndentAction: IndentAction, SuggestTriggerKind: modes.SuggestTriggerKind, - CommentThreadCollapsibleState: modes.CommentThreadCollapsibleState, - FoldingRangeKind: modes.FoldingRangeKind + FoldingRangeKind: modes.FoldingRangeKind, + SignatureHelpTriggerReason: modes.SignatureHelpTriggerReason, }; } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index e4f58e5df58..c6ca31a3c08 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -33,7 +33,7 @@ import { StandaloneCodeEditorServiceImpl } from 'vs/editor/standalone/browser/st import { SimpleConfigurationService, SimpleResourceConfigurationService, SimpleMenuService, SimpleProgressService, StandaloneCommandService, StandaloneKeybindingService, SimpleNotificationService, - StandaloneTelemetryService, SimpleWorkspaceContextService, SimpleDialogService, SimpleBulkEditService + StandaloneTelemetryService, SimpleWorkspaceContextService, SimpleDialogService, SimpleBulkEditService, SimpleUriLabelService } from 'vs/editor/standalone/browser/simpleServices'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IMenuService } from 'vs/platform/actions/common/actions'; @@ -44,11 +44,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; - -export interface IEditorContextViewService extends IContextViewService { - dispose(): void; - setContainer(domNode: HTMLElement): void; -} +import { ILabelService } from 'vs/platform/label/common/label'; export interface IEditorOverrideServices { [index: string]: any; @@ -126,6 +122,8 @@ export module StaticServices { export const contextService = define(IWorkspaceContextService, () => new SimpleWorkspaceContextService()); + export const labelService = define(ILabelService, () => new SimpleUriLabelService()); + export const telemetryService = define(ITelemetryService, () => new StandaloneTelemetryService()); export const dialogService = define(IDialogService, () => new SimpleDialogService()); diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 7f4f3612fc6..f72758ff75e 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -5,7 +5,7 @@ 'use strict'; import { TokenTheme, ITokenThemeRule, generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; -import { IStandaloneThemeService, BuiltinTheme, IStandaloneThemeData, IStandaloneTheme, IColors } from 'vs/editor/standalone/common/standaloneThemeService'; +import { IStandaloneThemeService, BuiltinTheme, IStandaloneThemeData, IStandaloneTheme } from 'vs/editor/standalone/common/standaloneThemeService'; import { vs, vs_dark, hc_black } from 'vs/editor/standalone/common/themes'; import * as dom from 'vs/base/browser/dom'; import { TokenizationRegistry } from 'vs/editor/common/modes'; @@ -26,13 +26,15 @@ const themingRegistry = Registry.as(ThemingExtensions.ThemingC class StandaloneTheme implements IStandaloneTheme { public readonly id: string; public readonly themeName: string; - private rules: ITokenThemeRule[]; - public readonly base: string; + + private themeData: IStandaloneThemeData; private colors: { [colorId: string]: Color }; private defaultColors: { [colorId: string]: Color }; private _tokenTheme: TokenTheme; - constructor(base: string, name: string, colors: IColors, rules: ITokenThemeRule[]) { + constructor(name: string, standaloneThemeData: IStandaloneThemeData) { + this.themeData = standaloneThemeData; + let base = standaloneThemeData.base; if (name.length > 0) { this.id = base + ' ' + name; this.themeName = name; @@ -40,18 +42,46 @@ class StandaloneTheme implements IStandaloneTheme { this.id = base; this.themeName = base; } - this.base = base; - this.rules = rules; - this.colors = {}; - for (let id in colors) { - this.colors[id] = Color.fromHex(colors[id]); + this.colors = null; + this.defaultColors = Object.create(null); + this._tokenTheme = null; + } + + public get base(): string { + return this.themeData.base; + } + + public notifyBaseUpdated() { + if (this.themeData.inherit) { + this.colors = null; + this._tokenTheme = null; } - this.defaultColors = {}; + } + + private getColors(): { [colorId: string]: Color } { + if (!this.colors) { + let colors: { [colorId: string]: Color } = Object.create(null); + for (let id in this.themeData.colors) { + colors[id] = Color.fromHex(this.themeData.colors[id]); + } + if (this.themeData.inherit) { + let baseData = getBuiltinRules(this.themeData.base); + for (let id in baseData.colors) { + if (!colors[id]) { + colors[id] = Color.fromHex(baseData.colors[id]); + } + + } + } + this.colors = colors; + } + return this.colors; } public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color { - if (this.colors.hasOwnProperty(colorId)) { - return this.colors[colorId]; + const color = this.getColors()[colorId]; + if (color) { + return color; } if (useDefault !== false) { return this.getDefault(colorId); @@ -60,16 +90,17 @@ class StandaloneTheme implements IStandaloneTheme { } private getDefault(colorId: ColorIdentifier): Color { - if (this.defaultColors.hasOwnProperty(colorId)) { - return this.defaultColors[colorId]; + let color = this.defaultColors[colorId]; + if (color) { + return color; } - let color = colorRegistry.resolveDefaultColor(colorId, this); + color = colorRegistry.resolveDefaultColor(colorId, this); this.defaultColors[colorId] = color; return color; } public defines(colorId: ColorIdentifier): boolean { - return this.colors.hasOwnProperty(colorId); + return Object.prototype.hasOwnProperty.call(this.getColors(), colorId); } public get type() { @@ -82,7 +113,20 @@ class StandaloneTheme implements IStandaloneTheme { public get tokenTheme(): TokenTheme { if (!this._tokenTheme) { - this._tokenTheme = TokenTheme.createFromRawTokenTheme(this.rules); + let rules: ITokenThemeRule[] = []; + let encodedTokensColors = []; + if (this.themeData.inherit) { + let baseData = getBuiltinRules(this.themeData.base); + rules = baseData.rules; + if (baseData.encodedTokensColors) { + encodedTokensColors = baseData.encodedTokensColors; + } + } + rules = rules.concat(this.themeData.rules); + if (this.themeData.encodedTokensColors) { + encodedTokensColors = this.themeData.encodedTokensColors; + } + this._tokenTheme = TokenTheme.createFromRawTokenTheme(rules, encodedTokensColors); } return this._tokenTheme; } @@ -109,7 +153,7 @@ function getBuiltinRules(builtinTheme: BuiltinTheme): IStandaloneThemeData { function newBuiltInTheme(builtinTheme: BuiltinTheme): StandaloneTheme { let themeData = getBuiltinRules(builtinTheme); - return new StandaloneTheme(builtinTheme, '', themeData.colors, themeData.rules); + return new StandaloneTheme(builtinTheme, themeData); } export class StandaloneThemeServiceImpl implements IStandaloneThemeService { @@ -139,28 +183,25 @@ export class StandaloneThemeServiceImpl implements IStandaloneThemeService { } public defineTheme(themeName: string, themeData: IStandaloneThemeData): void { - if (!/^[a-z0-9\-]+$/i.test(themeName) || isBuiltinTheme(themeName)) { + if (!/^[a-z0-9\-]+$/i.test(themeName)) { throw new Error('Illegal theme name!'); } - if (!isBuiltinTheme(themeData.base)) { + if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) { throw new Error('Illegal theme base!'); } + // set or replace theme + this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData)); - let rules: ITokenThemeRule[] = []; - let colors: IColors = {}; - if (themeData.inherit) { - let baseData = getBuiltinRules(themeData.base); - rules = rules.concat(baseData.rules); - for (let id in baseData.colors) { - colors[id] = baseData.colors[id]; - } + if (isBuiltinTheme(themeName)) { + this._knownThemes.forEach(theme => { + if (theme.base === themeName) { + theme.notifyBaseUpdated(); + } + }); } - rules = rules.concat(themeData.rules); - for (let id in themeData.colors) { - colors[id] = themeData.colors[id]; + if (this._theme && this._theme.themeName === themeName) { + this.setTheme(themeName); // refresh theme } - - this._knownThemes.set(themeName, new StandaloneTheme(themeData.base, themeName, colors, rules)); } public getTheme(): IStandaloneTheme { diff --git a/src/vs/editor/standalone/common/monarch/monarchTypes.ts b/src/vs/editor/standalone/common/monarch/monarchTypes.ts index 55a14165507..a2b88685fb2 100644 --- a/src/vs/editor/standalone/common/monarch/monarchTypes.ts +++ b/src/vs/editor/standalone/common/monarch/monarchTypes.ts @@ -37,7 +37,7 @@ export interface IMonarchLanguage { /** * attach this to every token class (by default '.' + name) */ - tokenPostfix: string; + tokenPostfix?: string; } /** @@ -45,7 +45,11 @@ export interface IMonarchLanguage { * shorthands: [reg,act] == { regex: reg, action: act} * and : [reg,act,nxt] == { regex: reg, action: act{ next: nxt }} */ -export interface IMonarchLanguageRule { +export type IShortMonarchLanguageRule1 = [RegExp, IMonarchLanguageAction]; + +export type IShortMonarchLanguageRule2 = [RegExp, IMonarchLanguageAction, string]; + +export interface IExpandedMonarchLanguageRule { /** * match tokens */ @@ -61,22 +65,26 @@ export interface IMonarchLanguageRule { include?: string; } +export type IMonarchLanguageRule = IShortMonarchLanguageRule1 + | IShortMonarchLanguageRule2 + | IExpandedMonarchLanguageRule; + /** * An action is either an array of actions... * ... or a case statement with guards... * ... or a basic action with a token value. */ -export interface IMonarchLanguageAction { +export type IShortMonarchLanguageAction = string; + +export interface IExpandedMonarchLanguageAction { /** * array of actions for each parenthesized match group */ group?: IMonarchLanguageAction[]; - /** * map from string to ILanguageAction */ cases?: Object; - /** * token class (ie. css class) (or "@brackets" or "@rematch") */ @@ -107,6 +115,11 @@ export interface IMonarchLanguageAction { log?: string; } +export type IMonarchLanguageAction = IShortMonarchLanguageAction + | IExpandedMonarchLanguageAction + | IShortMonarchLanguageAction[] + | IExpandedMonarchLanguageAction[]; + /** * This interface can be shortened as an array, ie. ['{','}','delimiter.curly'] */ @@ -123,4 +136,4 @@ export interface IMonarchLanguageBracket { * token class */ token: string; -} +} \ No newline at end of file diff --git a/src/vs/editor/standalone/common/standaloneThemeService.ts b/src/vs/editor/standalone/common/standaloneThemeService.ts index 7dfc0bdbdfb..12e5b7782cd 100644 --- a/src/vs/editor/standalone/common/standaloneThemeService.ts +++ b/src/vs/editor/standalone/common/standaloneThemeService.ts @@ -17,6 +17,7 @@ export interface IStandaloneThemeData { base: BuiltinTheme; inherit: boolean; rules: ITokenThemeRule[]; + encodedTokensColors?: string[]; colors: IColors; } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 7c4d3136f27..d744095cd1e 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3947,6 +3947,21 @@ suite('autoClosingPairs', () => { ], })); } + + public setAutocloseEnabledSet(chars: string) { + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + autoCloseBefore: chars, + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } + ], + })); + } } const enum ColumnType { @@ -3982,7 +3997,7 @@ suite('autoClosingPairs', () => { cursorCommand(cursor, H.Undo); } - test('open parens', () => { + test('open parens: default', () => { let mode = new AutoClosingMode(); usingCursor({ text: [ @@ -3998,6 +4013,52 @@ suite('autoClosingPairs', () => { languageIdentifier: mode.getLanguageIdentifier() }, (model, cursor) => { + let autoClosePositions = [ + 'var| a| |=| [|]|;|', + 'var| b| |=| `asd`|;|', + 'var| c| |=| \'asd\'|;|', + 'var| d| |=| "asd"|;|', + 'var| e| |=| /*3*/| 3|;|', + 'var| f| |=| /**| 3| */3|;|', + 'var| g| |=| (3+5|)|;|', + 'var| h| |=| {| a|:| \'value\'| |}|;|', + ]; + for (let i = 0, len = autoClosePositions.length; i < len; i++) { + const lineNumber = i + 1; + const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + + for (let column = 1; column < autoCloseColumns.length; column++) { + model.forceTokenization(lineNumber); + if (autoCloseColumns[column] === ColumnType.Special1) { + assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + } else { + assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + } + } + } + }); + mode.dispose(); + }); + + test('open parens: whitespace', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'var a = [];', + 'var b = `asd`;', + 'var c = \'asd\';', + 'var d = "asd";', + 'var e = /*3*/ 3;', + 'var f = /** 3 */3;', + 'var g = (3+5);', + 'var h = { a: \'value\' };', + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoClosingBrackets: 'beforeWhitespace' + } + }, (model, cursor) => { + let autoClosePositions = [ 'var| a| =| [|];|', 'var| b| =| `asd`;|', @@ -4025,6 +4086,259 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('open parens disabled/enabled open quotes enabled/disabled', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'var a = [];', + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoClosingBrackets: 'beforeWhitespace', + autoClosingQuotes: 'never' + } + }, (model, cursor) => { + + let autoClosePositions = [ + 'var| a| =| [|];|', + ]; + for (let i = 0, len = autoClosePositions.length; i < len; i++) { + const lineNumber = i + 1; + const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + + for (let column = 1; column < autoCloseColumns.length; column++) { + model.forceTokenization(lineNumber); + if (autoCloseColumns[column] === ColumnType.Special1) { + assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + } else { + assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + } + assertType(model, cursor, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); + } + } + }); + + usingCursor({ + text: [ + 'var b = [];', + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoClosingBrackets: 'never', + autoClosingQuotes: 'beforeWhitespace' + } + }, (model, cursor) => { + + let autoClosePositions = [ + 'var b =| [|];|', + ]; + for (let i = 0, len = autoClosePositions.length; i < len; i++) { + const lineNumber = i + 1; + const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + + for (let column = 1; column < autoCloseColumns.length; column++) { + model.forceTokenization(lineNumber); + if (autoCloseColumns[column] === ColumnType.Special1) { + assertType(model, cursor, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); + } else { + assertType(model, cursor, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); + } + assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + } + } + }); + mode.dispose(); + }); + + test('configurable open parens', () => { + let mode = new AutoClosingMode(); + mode.setAutocloseEnabledSet('abc'); + usingCursor({ + text: [ + 'var a = [];', + 'var b = `asd`;', + 'var c = \'asd\';', + 'var d = "asd";', + 'var e = /*3*/ 3;', + 'var f = /** 3 */3;', + 'var g = (3+5);', + 'var h = { a: \'value\' };', + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoClosingBrackets: 'languageDefined' + } + }, (model, cursor) => { + + let autoClosePositions = [ + 'v|ar |a = [|];|', + 'v|ar |b = `|asd`;|', + 'v|ar |c = \'|asd\';|', + 'v|ar d = "|asd";|', + 'v|ar e = /*3*/ 3;|', + 'v|ar f = /** 3 */3;|', + 'v|ar g = (3+5|);|', + 'v|ar h = { |a: \'v|alue\' |};|', + ]; + for (let i = 0, len = autoClosePositions.length; i < len; i++) { + const lineNumber = i + 1; + const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + + for (let column = 1; column < autoCloseColumns.length; column++) { + model.forceTokenization(lineNumber); + if (autoCloseColumns[column] === ColumnType.Special1) { + assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + } else { + assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + } + } + } + }); + mode.dispose(); + }); + + test('auto-pairing can be disabled', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'var a = [];', + 'var b = `asd`;', + 'var c = \'asd\';', + 'var d = "asd";', + 'var e = /*3*/ 3;', + 'var f = /** 3 */3;', + 'var g = (3+5);', + 'var h = { a: \'value\' };', + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoClosingBrackets: 'never', + autoClosingQuotes: 'never' + } + }, (model, cursor) => { + + let autoClosePositions = [ + 'var a = [];', + 'var b = `asd`;', + 'var c = \'asd\';', + 'var d = "asd";', + 'var e = /*3*/ 3;', + 'var f = /** 3 */3;', + 'var g = (3+5);', + 'var h = { a: \'value\' };', + ]; + for (let i = 0, len = autoClosePositions.length; i < len; i++) { + const lineNumber = i + 1; + const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + + for (let column = 1; column < autoCloseColumns.length; column++) { + model.forceTokenization(lineNumber); + if (autoCloseColumns[column] === ColumnType.Special1) { + assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(model, cursor, lineNumber, column, '"', '""', `auto closes @ (${lineNumber}, ${column})`); + } else { + assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(model, cursor, lineNumber, column, '"', '"', `does not auto close @ (${lineNumber}, ${column})`); + } + } + } + }); + mode.dispose(); + }); + + test('auto wrapping is configurable', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'var a = asd' + ], + languageIdentifier: mode.getLanguageIdentifier() + }, (model, cursor) => { + + cursor.setSelections('test', [ + new Selection(1, 1, 1, 4), + new Selection(1, 9, 1, 12), + ]); + + // type a ` + cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + + assert.equal(model.getValue(), '`var` a = `asd`'); + + // type a ( + cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + + assert.equal(model.getValue(), '`(var)` a = `(asd)`'); + }); + + usingCursor({ + text: [ + 'var a = asd' + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoSurround: 'never' + } + }, (model, cursor) => { + + cursor.setSelections('test', [ + new Selection(1, 1, 1, 4), + ]); + + // type a ` + cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + + assert.equal(model.getValue(), '` a = asd'); + }); + + usingCursor({ + text: [ + 'var a = asd' + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoSurround: 'quotes' + } + }, (model, cursor) => { + + cursor.setSelections('test', [ + new Selection(1, 1, 1, 4), + ]); + + // type a ` + cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + assert.equal(model.getValue(), '`var` a = asd'); + + // type a ( + cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + assert.equal(model.getValue(), '`(` a = asd'); + }); + + usingCursor({ + text: [ + 'var a = asd' + ], + languageIdentifier: mode.getLanguageIdentifier(), + editorOpts: { + autoSurround: 'brackets' + } + }, (model, cursor) => { + + cursor.setSelections('test', [ + new Selection(1, 1, 1, 4), + ]); + + // type a ( + cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + assert.equal(model.getValue(), '(var) a = asd'); + + // type a ` + cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + assert.equal(model.getValue(), '(`) a = asd'); + }); + mode.dispose(); + }); + test('quote', () => { let mode = new AutoClosingMode(); usingCursor({ @@ -4042,14 +4356,14 @@ suite('autoClosingPairs', () => { }, (model, cursor) => { let autoClosePositions = [ - 'var a =| [|];|', - 'var b =| |`asd`;|', - 'var c =| |\'asd!\';|', - 'var d =| |"asd";|', - 'var e =| /*3*/| 3;|', - 'var f =| /**| 3 */3;|', - 'var g =| (3+5);|', - 'var h =| {| a:| |\'value!\'| |};|', + 'var a |=| [|]|;|', + 'var b |=| |`asd`|;|', + 'var c |=| |\'asd!\'|;|', + 'var d |=| |"asd"|;|', + 'var e |=| /*3*/| 3;|', + 'var f |=| /**| 3 */3;|', + 'var g |=| (3+5)|;|', + 'var h |=| {| a:| |\'value!\'| |}|;|', ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; @@ -4070,6 +4384,51 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('issue #55314: Do not auto-close when ending with open', () => { + const languageId = new LanguageIdentifier('myElectricMode', 5); + class ElectricMode extends MockMode { + constructor() { + super(languageId); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: 'B\"', close: '\"', notIn: ['string', 'comment'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } + ], + })); + } + } + + const mode = new ElectricMode(); + + usingCursor({ + text: [ + 'little goat', + 'little LAMB', + 'little sheep', + 'Big LAMB' + ], + languageIdentifier: mode.getLanguageIdentifier() + }, (model, cursor) => { + model.forceTokenization(model.getLineCount()); + assertType(model, cursor, 1, 4, '"', '"', `does not double quote when ending with open`); + model.forceTokenization(model.getLineCount()); + assertType(model, cursor, 2, 4, '"', '"', `does not double quote when ending with open`); + model.forceTokenization(model.getLineCount()); + assertType(model, cursor, 3, 4, '"', '"', `does not double quote when ending with open`); + model.forceTokenization(model.getLineCount()); + assertType(model, cursor, 4, 2, '"', '""', `double quote when ending with open`); + model.forceTokenization(model.getLineCount()); + assertType(model, cursor, 4, 3, '"', '"', `does not double quote when ending with open`); + }); + mode.dispose(); + }); + test('issue #27937: Trying to add an item to the front of a list is cumbersome', () => { let mode = new AutoClosingMode(); usingCursor({ @@ -4222,7 +4581,7 @@ suite('autoClosingPairs', () => { cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '"' }, 'keyboard'); cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); - assert.equal(model.getValue(), '\'""\''); + assert.equal(model.getValue(), '\'"\''); // Typing ' + space after ' model.setValue('\''); diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 492ad210207..a954c51f42e 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -518,6 +518,20 @@ suite('TextAreaState', () => { ); }); + test('issue #49480: Double curly braces inserted', () => { + // Characters get doubled + testDeduceInput( + new TextAreaState( + 'aa', + 2, 2, + null, null + ), + 'aaa', + 3, 3, true, true, + 'a', 0 + ); + }); + suite('PagedScreenReaderStrategy', () => { function testPagedScreenReaderStrategy(lines: string[], selection: Selection, expected: TextAreaState): void { diff --git a/src/vs/editor/test/browser/core/editorState.test.ts b/src/vs/editor/test/browser/core/editorState.test.ts index 5441477e432..e3e64299bb4 100644 --- a/src/vs/editor/test/browser/core/editorState.test.ts +++ b/src/vs/editor/test/browser/core/editorState.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 72291114149..0c31be1e257 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 0401ca8b4ef..dfe92ebf965 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 79ae7e5d017..4a0d4669624 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -72,10 +72,14 @@ export interface TestCodeEditorCreationOptions extends editorOptions.IEditorOpti serviceCollection?: ServiceCollection; } -export function withTestCodeEditor(text: string[], options: TestCodeEditorCreationOptions, callback: (editor: TestCodeEditor, cursor: Cursor) => void): void { +export function withTestCodeEditor(text: string | string[], options: TestCodeEditorCreationOptions, callback: (editor: TestCodeEditor, cursor: Cursor) => void): void { // create a model if necessary and remember it in order to dispose it. if (!options.model) { - options.model = TextModel.createFromString(text.join('\n')); + if (typeof text === 'string') { + options.model = TextModel.createFromString(text); + } else { + options.model = TextModel.createFromString(text.join('\n')); + } } let editor = createTestCodeEditor(options); diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index 92f7a852d58..c2d68367dcc 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -9,6 +9,7 @@ import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { IEnvConfiguration } from 'vs/editor/common/config/commonEditorConfig'; import { AccessibilitySupport } from 'vs/base/common/platform'; +import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; suite('Common Editor Config', () => { test('Zoom Level', () => { @@ -176,4 +177,17 @@ suite('Common Editor Config', () => { }); assertWrapping(config, true, 1); }); + + test('issue #53152: Cannot assign to read only property \'enabled\' of object', () => { + let hoverOptions: IEditorHoverOptions = {}; + Object.defineProperty(hoverOptions, 'enabled', { + writable: false, + value: true + }); + let config = new TestConfiguration({ hover: hoverOptions }); + + assert.equal(config.editor.contribInfo.hover.enabled, true); + config.updateOptions({ hover: { enabled: false } }); + assert.equal(config.editor.contribInfo.hover.enabled, false); + }); }); diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 2a7cc783ee3..574a75000ee 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -25,8 +25,9 @@ function extractCharChangeRepresentation(change: ICharChange, expectedChange: IC } function extractLineChangeRepresentation(change: ILineChange, expectedChange: ILineChange): IChange | ILineChange { + let charChanges: ICharChange[]; if (change.charChanges) { - let charChanges: ICharChange[] = []; + charChanges = []; for (let i = 0; i < change.charChanges.length; i++) { charChanges.push( extractCharChangeRepresentation( @@ -35,26 +36,21 @@ function extractLineChangeRepresentation(change: ILineChange, expectedChange: IL ) ); } - return { - originalStartLineNumber: change.originalStartLineNumber, - originalEndLineNumber: change.originalEndLineNumber, - modifiedStartLineNumber: change.modifiedStartLineNumber, - modifiedEndLineNumber: change.modifiedEndLineNumber, - charChanges: charChanges - }; } return { originalStartLineNumber: change.originalStartLineNumber, originalEndLineNumber: change.originalEndLineNumber, modifiedStartLineNumber: change.modifiedStartLineNumber, - modifiedEndLineNumber: change.modifiedEndLineNumber + modifiedEndLineNumber: change.modifiedEndLineNumber, + charChanges: charChanges }; } -function assertDiff(originalLines: string[], modifiedLines: string[], expectedChanges: IChange[], shouldPostProcessCharChanges: boolean = false, shouldIgnoreTrimWhitespace: boolean = false) { +function assertDiff(originalLines: string[], modifiedLines: string[], expectedChanges: IChange[], shouldComputeCharChanges: boolean = true, shouldPostProcessCharChanges: boolean = false, shouldIgnoreTrimWhitespace: boolean = false) { let diffComputer = new DiffComputer(originalLines, modifiedLines, { - shouldPostProcessCharChanges: shouldPostProcessCharChanges || false, - shouldIgnoreTrimWhitespace: shouldIgnoreTrimWhitespace || false, + shouldComputeCharChanges, + shouldPostProcessCharChanges, + shouldIgnoreTrimWhitespace, shouldMakePrettyDiff: true }); let changes = diffComputer.computeDiff(); @@ -66,25 +62,27 @@ function assertDiff(originalLines: string[], modifiedLines: string[], expectedCh assert.deepEqual(extracted, expectedChanges); } -function createLineDeletion(startLineNumber: number, endLineNumber: number, modifiedLineNumber: number): IChange { +function createLineDeletion(startLineNumber: number, endLineNumber: number, modifiedLineNumber: number): ILineChange { return { originalStartLineNumber: startLineNumber, originalEndLineNumber: endLineNumber, modifiedStartLineNumber: modifiedLineNumber, - modifiedEndLineNumber: 0 + modifiedEndLineNumber: 0, + charChanges: undefined }; } -function createLineInsertion(startLineNumber: number, endLineNumber: number, originalLineNumber: number): IChange { +function createLineInsertion(startLineNumber: number, endLineNumber: number, originalLineNumber: number): ILineChange { return { originalStartLineNumber: originalLineNumber, originalEndLineNumber: 0, modifiedStartLineNumber: startLineNumber, - modifiedEndLineNumber: endLineNumber + modifiedEndLineNumber: endLineNumber, + charChanges: undefined }; } -function createLineChange(originalStartLineNumber: number, originalEndLineNumber: number, modifiedStartLineNumber: number, modifiedEndLineNumber: number, charChanges: ICharChange[]): ILineChange { +function createLineChange(originalStartLineNumber: number, originalEndLineNumber: number, modifiedStartLineNumber: number, modifiedEndLineNumber: number, charChanges?: ICharChange[]): ILineChange { return { originalStartLineNumber: originalStartLineNumber, originalEndLineNumber: originalEndLineNumber, @@ -390,7 +388,7 @@ suite('Editor Diff - DiffComputer', () => { createCharChange(1, 2, 1, 4, 1, 2, 1, 13) ]) ]; - assertDiff(original, modified, expected, true); + assertDiff(original, modified, expected, true, true); }); test('ignore trim whitespace', () => { @@ -403,7 +401,7 @@ suite('Editor Diff - DiffComputer', () => { createCharInsertion(4, 1, 4, 4) ]) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('issue #12122 r.hasOwnProperty is not a function', () => { @@ -423,7 +421,7 @@ suite('Editor Diff - DiffComputer', () => { createCharChange(0, 0, 0, 0, 0, 0, 0, 0) ]) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('empty diff 2', () => { @@ -434,7 +432,7 @@ suite('Editor Diff - DiffComputer', () => { createCharChange(0, 0, 0, 0, 0, 0, 0, 0) ]) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('empty diff 3', () => { @@ -445,7 +443,7 @@ suite('Editor Diff - DiffComputer', () => { createCharChange(0, 0, 0, 0, 0, 0, 0, 0) ]) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('empty diff 4', () => { @@ -456,7 +454,7 @@ suite('Editor Diff - DiffComputer', () => { createCharChange(0, 0, 0, 0, 0, 0, 0, 0) ]) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('pretty diff 1', () => { @@ -493,7 +491,7 @@ suite('Editor Diff - DiffComputer', () => { createLineInsertion(1, 1, 0), createLineInsertion(10, 13, 8) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('pretty diff 2', () => { @@ -536,7 +534,7 @@ suite('Editor Diff - DiffComputer', () => { createLineInsertion(1, 3, 0), createLineDeletion(10, 13, 12), ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('pretty diff 3', () => { @@ -574,7 +572,7 @@ suite('Editor Diff - DiffComputer', () => { let expected = [ createLineInsertion(7, 11, 6) ]; - assertDiff(original, modified, expected, false, true); + assertDiff(original, modified, expected, true, false, true); }); test('issue #23636', () => { @@ -671,7 +669,7 @@ suite('Editor Diff - DiffComputer', () => { ) // createLineInsertion(7, 11, 6) ]; - assertDiff(original, modified, expected, true, false); + assertDiff(original, modified, expected, true, true, false); }); test('issue #43922', () => { @@ -690,7 +688,7 @@ suite('Editor Diff - DiffComputer', () => { ] ) ]; - assertDiff(original, modified, expected, true, false); + assertDiff(original, modified, expected, true, true, false); }); test('issue #42751', () => { @@ -710,6 +708,25 @@ suite('Editor Diff - DiffComputer', () => { ] ) ]; - assertDiff(original, modified, expected, true, false); + assertDiff(original, modified, expected, true, true, false); + }); + + test('does not give character changes', () => { + let original = [ + ' 1', + ' 2', + 'A', + ]; + let modified = [ + ' 1', + ' 3', + ' A', + ]; + let expected = [ + createLineChange( + 2, 3, 2, 3 + ) + ]; + assertDiff(original, modified, expected, false, false, false); }); }); diff --git a/src/vs/editor/test/common/editorTestUtils.ts b/src/vs/editor/test/common/editorTestUtils.ts index c97a9a55a74..f02eab651ac 100644 --- a/src/vs/editor/test/common/editorTestUtils.ts +++ b/src/vs/editor/test/common/editorTestUtils.ts @@ -7,7 +7,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { DefaultEndOfLine, ITextModelCreationOptions } from 'vs/editor/common/model'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export function withEditorModel(text: string[], callback: (model: TextModel) => void): void { let model = TextModel.createFromString(text.join('\n')); diff --git a/src/vs/editor/test/common/mocks/testConfiguration.ts b/src/vs/editor/test/common/mocks/testConfiguration.ts index b94830a8456..0492346bff7 100644 --- a/src/vs/editor/test/common/mocks/testConfiguration.ts +++ b/src/vs/editor/test/common/mocks/testConfiguration.ts @@ -39,6 +39,7 @@ export class TestConfiguration extends CommonEditorConfiguration { isMonospace: true, typicalHalfwidthCharacterWidth: 10, typicalFullwidthCharacterWidth: 20, + canUseHalfwidthRightwardsArrow: true, spaceWidth: 10, maxDigitWidth: 10, }, true); diff --git a/src/vs/editor/test/common/model/benchmark/bootstrap.js b/src/vs/editor/test/common/model/benchmark/bootstrap.js index a93aabeb980..4364204ce0a 100644 --- a/src/vs/editor/test/common/model/benchmark/bootstrap.js +++ b/src/vs/editor/test/common/model/benchmark/bootstrap.js @@ -3,4 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -require('../../../../../../bootstrap-amd').bootstrap('vs/editor/test/common/model/benchmark/entry'); \ No newline at end of file +require('../../../../../../bootstrap-amd').load('vs/editor/test/common/model/benchmark/entry'); \ No newline at end of file diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index fa061f82b6d..96a16b357b4 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { score } from 'vs/editor/common/modes/languageSelector'; suite('LanguageSelector', function () { diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 199a670cdaf..755d5b9d2f4 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -15,7 +15,7 @@ suite('Token theme matching', () => { { token: '', foreground: '100000', background: '200000' }, { token: 'punctuation.definition.string.begin.html', foreground: '300000' }, { token: 'punctuation.definition.string', foreground: '400000' }, - ]); + ], []); let colorMap = new ColorMap(); colorMap.getId('100000'); @@ -42,7 +42,7 @@ suite('Token theme matching', () => { { token: 'constant.numeric.oct', fontStyle: 'bold italic underline' }, { token: 'constant.numeric.dec', fontStyle: '', foreground: '500000' }, { token: 'storage.object.bar', fontStyle: '', foreground: '600000' }, - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('F8F8F2'); @@ -167,7 +167,7 @@ suite('Token theme resolving', () => { }); test('always has defaults', () => { - let actual = TokenTheme.createFromParsedTokenTheme([]); + let actual = TokenTheme.createFromParsedTokenTheme([], []); let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); @@ -178,7 +178,7 @@ suite('Token theme resolving', () => { test('respects incoming defaults 1', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.NotSet, null, null) - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); @@ -189,7 +189,7 @@ suite('Token theme resolving', () => { test('respects incoming defaults 2', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.None, null, null) - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); @@ -200,7 +200,7 @@ suite('Token theme resolving', () => { test('respects incoming defaults 3', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.Bold, null, null) - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); @@ -211,7 +211,7 @@ suite('Token theme resolving', () => { test('respects incoming defaults 4', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.NotSet, 'ff0000', null) - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('ff0000'); const _B = colorMap.getId('ffffff'); @@ -222,7 +222,7 @@ suite('Token theme resolving', () => { test('respects incoming defaults 5', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.NotSet, null, 'ff0000') - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ff0000'); @@ -235,7 +235,7 @@ suite('Token theme resolving', () => { new ParsedTokenThemeRule('', -1, FontStyle.NotSet, null, 'ff0000'), new ParsedTokenThemeRule('', -1, FontStyle.NotSet, '00ff00', null), new ParsedTokenThemeRule('', -1, FontStyle.Bold, null, null), - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('00ff00'); const _B = colorMap.getId('ff0000'); @@ -247,7 +247,7 @@ suite('Token theme resolving', () => { let actual = TokenTheme.createFromParsedTokenTheme([ new ParsedTokenThemeRule('', -1, FontStyle.NotSet, 'F8F8F2', '272822'), new ParsedTokenThemeRule('var', -1, FontStyle.NotSet, 'ff0000', null) - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); @@ -264,7 +264,7 @@ suite('Token theme resolving', () => { new ParsedTokenThemeRule('', -1, FontStyle.NotSet, 'F8F8F2', '272822'), new ParsedTokenThemeRule('var', 1, FontStyle.Bold, null, null), new ParsedTokenThemeRule('var', 0, FontStyle.NotSet, 'ff0000', null), - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); @@ -281,7 +281,7 @@ suite('Token theme resolving', () => { new ParsedTokenThemeRule('', -1, FontStyle.NotSet, 'F8F8F2', '272822'), new ParsedTokenThemeRule('var', -1, FontStyle.Bold, 'ff0000', null), new ParsedTokenThemeRule('var.identifier', -1, FontStyle.NotSet, '00ff00', null), - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); @@ -306,7 +306,7 @@ suite('Token theme resolving', () => { new ParsedTokenThemeRule('constant.numeric.hex', 6, FontStyle.Bold, null, null), new ParsedTokenThemeRule('constant.numeric.oct', 7, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, null, null), new ParsedTokenThemeRule('constant.numeric.dec', 8, FontStyle.None, '300000', null), - ]); + ], []); let colorMap = new ColorMap(); const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); @@ -330,4 +330,18 @@ suite('Token theme resolving', () => { }); assert.deepEqual(actual.getThemeTrieElement(), root); }); + + test('custom colors are first in color map', () => { + let actual = TokenTheme.createFromParsedTokenTheme([ + new ParsedTokenThemeRule('var', -1, FontStyle.NotSet, 'F8F8F2', null) + ], [ + '000000', 'FFFFFF', '0F0F0F' + ]); + let colorMap = new ColorMap(); + colorMap.getId('000000'); + colorMap.getId('FFFFFF'); + colorMap.getId('0F0F0F'); + colorMap.getId('F8F8F2'); + assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + }); }); diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 42ed949ff4f..cf2b1f618d9 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; +import { URI } from 'vs/base/common/uri'; suite('LanguagesRegistry', () => { @@ -250,19 +251,19 @@ suite('LanguagesRegistry', () => { registry._registerLanguages([{ id: 'a', aliases: ['aName'], - configuration: 'aFilename' + configuration: URI.file('/path/to/aFilename') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), ['aFilename']); + assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); assert.deepEqual(registry.getConfigurationFiles('aname'), []); assert.deepEqual(registry.getConfigurationFiles('aName'), []); registry._registerLanguages([{ id: 'a', - configuration: 'aFilename2' + configuration: URI.file('/path/to/aFilename2') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), ['aFilename', 'aFilename2']); + assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); assert.deepEqual(registry.getConfigurationFiles('aname'), []); assert.deepEqual(registry.getConfigurationFiles('aName'), []); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index fa75cbb9c4f..b4db0be2cc7 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; diff --git a/src/vs/editor/test/common/standalone/standaloneBase.test.ts b/src/vs/editor/test/common/standalone/standaloneBase.test.ts index 64849eb80cb..4cec807f3c1 100644 --- a/src/vs/editor/test/common/standalone/standaloneBase.test.ts +++ b/src/vs/editor/test/common/standalone/standaloneBase.test.ts @@ -5,18 +5,8 @@ 'use strict'; import * as assert from 'assert'; -import { KeyCode as StandaloneKeyCode, Severity as StandaloneSeverity } from 'vs/editor/common/standalone/standaloneBase'; +import { KeyCode as StandaloneKeyCode } from 'vs/editor/common/standalone/standaloneBase'; import { KeyCode as RuntimeKeyCode } from 'vs/base/common/keyCodes'; -import RuntimeSeverity from 'vs/base/common/severity'; - -suite('StandaloneBase', () => { - test('exports enums correctly', () => { - assert.equal(StandaloneSeverity.Ignore, RuntimeSeverity.Ignore); - assert.equal(StandaloneSeverity.Info, RuntimeSeverity.Info); - assert.equal(StandaloneSeverity.Warning, RuntimeSeverity.Warning); - assert.equal(StandaloneSeverity.Error, RuntimeSeverity.Error); - }); -}); suite('KeyCode', () => { test('is exported correctly in standalone editor', () => { diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 59705ec0d93..968b1a20126 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -29,6 +29,7 @@ suite('viewLineRenderer.renderLine', () => { function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void { let _actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, strings.isBasicASCII(lineContent), @@ -77,6 +78,7 @@ suite('viewLineRenderer.renderLine', () => { function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, expectedCharOffsetInPart: number[][], expectedPartLengts: number[]): void { let _actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, @@ -115,6 +117,7 @@ suite('viewLineRenderer.renderLine', () => { test('overflow', () => { let _actual = renderViewLine(new RenderLineInput( false, + true, 'Hello world!', false, true, @@ -218,6 +221,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, true, @@ -279,6 +283,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, true, @@ -340,6 +345,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, true, @@ -378,6 +384,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, false, @@ -407,6 +414,7 @@ suite('viewLineRenderer.renderLine', () => { let lineParts = createViewLineTokens([createPart(lineText.length, 1)]); let actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, true, @@ -506,6 +514,7 @@ suite('viewLineRenderer.renderLine', () => { let lineParts = createViewLineTokens([createPart(lineText.length, 1)]); let actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, true, @@ -541,6 +550,7 @@ suite('viewLineRenderer.renderLine', () => { let lineParts = createViewLineTokens([createPart(lineText.length, 1)]); let actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, false, @@ -569,6 +579,7 @@ suite('viewLineRenderer.renderLine', () => { ]; let actual = renderViewLine(new RenderLineInput( false, + true, lineText, false, false, @@ -613,6 +624,7 @@ suite('viewLineRenderer.renderLine', () => { ].join(''); let _actual = renderViewLine(new RenderLineInput( + true, true, lineText, false, @@ -696,6 +708,7 @@ suite('viewLineRenderer.renderLine 2', () => { function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'all', expected: string): void { let actual = renderViewLine(new RenderLineInput( fontIsMonospace, + true, lineContent, false, true, @@ -720,6 +733,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, @@ -749,6 +763,7 @@ suite('viewLineRenderer.renderLine 2', () => { let lineContent = '\'let url = `http://***/_api/web/lists/GetByTitle(\\\'Teambuildingaanvragen\\\')/items`;\''; let actual = renderViewLine(new RenderLineInput( + true, true, lineContent, false, @@ -1016,6 +1031,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('createLineParts can handle unsorted inline decorations', () => { let actual = renderViewLine(new RenderLineInput( false, + true, 'Hello world', false, true, @@ -1059,6 +1075,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, @@ -1090,6 +1107,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, @@ -1122,6 +1140,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, @@ -1149,6 +1168,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => { let actual = renderViewLine(new RenderLineInput( + true, true, ' 1. 🙏', false, @@ -1178,6 +1198,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #37401: Allow both before and after decorations on empty line', () => { let actual = renderViewLine(new RenderLineInput( + true, true, '', false, @@ -1209,6 +1230,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #38935: GitLens end-of-line blame no longer rendering', () => { let actual = renderViewLine(new RenderLineInput( + true, true, '\t}', false, @@ -1241,6 +1263,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #22832: Consider fullwidth characters when rendering tabs', () => { let actual = renderViewLine(new RenderLineInput( + true, true, 'asd = "擦"\t\t#asd', false, @@ -1269,6 +1292,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', () => { let actual = renderViewLine(new RenderLineInput( + true, true, 'asd = "擦"\t\t#asd', false, @@ -1303,6 +1327,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => { let actual = renderViewLine(new RenderLineInput( + true, true, '12345689012345678901234568901234567890123456890abába', false, @@ -1331,6 +1356,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => { let actual = renderViewLine(new RenderLineInput( + true, true, ' JoyShareல் பின்தொடர்ந்து, விடீயோ, ஜோக்குகள், அனிமேசன், நகைச்சுவை படங்கள் மற்றும் செய்திகளை பெறுவீர்', false, @@ -1363,6 +1389,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #42700: Hindi characters are not being rendered properly', () => { let actual = renderViewLine(new RenderLineInput( + true, true, ' वो ऐसा क्या है जो हमारे अंदर भी है और बाहर भी है। जिसकी वजह से हम सब हैं। जिसने इस सृष्टि की रचना की है।', false, @@ -1390,6 +1417,7 @@ suite('viewLineRenderer.renderLine 2', () => { test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => { let actual = renderViewLine(new RenderLineInput( + true, true, 'This is a long line which never uses more than two spaces. ', true, @@ -1418,6 +1446,7 @@ suite('viewLineRenderer.renderLine 2', () => { function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void { let renderLineOutput = renderViewLine(new RenderLineInput( false, + true, lineContent, false, true, diff --git a/src/vs/loader.js b/src/vs/loader.js index 291f0e8b6b3..d02070659ce 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -142,7 +142,7 @@ var AMDLoader; * This method does not take care of / vs \ */ Utilities.fileUriToFilePath = function (isWindows, uri) { - uri = decodeURI(uri); + uri = decodeURI(uri).replace(/%23/g, '#'); if (isWindows) { if (/^file:\/\/\//.test(uri)) { // This is a URI without a hostname => return only the path segment @@ -212,7 +212,7 @@ var AMDLoader; return '===anonymous' + (Utilities.NEXT_ANONYMOUS_ID++) + '==='; }; Utilities.isAnonymousModule = function (id) { - return /^===anonymous/.test(id); + return Utilities.startsWith(id, '===anonymous'); }; Utilities.getHighPerformanceTimestamp = function () { if (!this.PERFORMANCE_NOW_PROBED) { @@ -768,8 +768,7 @@ var AMDLoader; } contents = nodeInstrumenter(contents, normalizedScriptSrc); if (!opts.nodeCachedDataDir) { - _this._loadAndEvalScript(moduleManager, scriptSrc, vmScriptSrc, contents, { filename: vmScriptSrc }, recorder); - callback(); + _this._loadAndEvalScript(moduleManager, scriptSrc, vmScriptSrc, contents, { filename: vmScriptSrc }, recorder, callback, errorback); } else { var cachedDataPath_1 = _this._getCachedDataPath(opts.nodeCachedDataDir, scriptSrc); @@ -780,22 +779,34 @@ var AMDLoader; produceCachedData: typeof cachedData === 'undefined', cachedData: cachedData }; - var script = _this._loadAndEvalScript(moduleManager, scriptSrc, vmScriptSrc, contents, options, recorder); - callback(); + var script = _this._loadAndEvalScript(moduleManager, scriptSrc, vmScriptSrc, contents, options, recorder, callback, errorback); _this._processCachedData(moduleManager, script, cachedDataPath_1); }); } }); } }; - NodeScriptLoader.prototype._loadAndEvalScript = function (moduleManager, scriptSrc, vmScriptSrc, contents, options, recorder) { + NodeScriptLoader.prototype._loadAndEvalScript = function (moduleManager, scriptSrc, vmScriptSrc, contents, options, recorder, callback, errorback) { // create script, run script recorder.record(31 /* NodeBeginEvaluatingScript */, scriptSrc); var script = new this._vm.Script(contents, options); var r = script.runInThisContext(options); - r.call(AMDLoader.global, moduleManager.getGlobalAMDRequireFunc(), moduleManager.getGlobalAMDDefineFunc(), vmScriptSrc, this._path.dirname(scriptSrc)); + var globalDefineFunc = moduleManager.getGlobalAMDDefineFunc(); + var receivedDefineCall = false; + var localDefineFunc = function () { + receivedDefineCall = true; + return globalDefineFunc.apply(null, arguments); + }; + localDefineFunc.amd = globalDefineFunc.amd; + r.call(AMDLoader.global, moduleManager.getGlobalAMDRequireFunc(), localDefineFunc, vmScriptSrc, this._path.dirname(scriptSrc)); // signal done recorder.record(32 /* NodeEndEvaluatingScript */, scriptSrc); + if (receivedDefineCall) { + callback(); + } + else { + errorback(new Error("Didn't receive define call in " + scriptSrc + "!")); + } return script; }; NodeScriptLoader.prototype._getCachedDataPath = function (basedir, filename) { @@ -1403,7 +1414,8 @@ var AMDLoader; this._knownModules2[moduleId] = true; var strModuleId = this._moduleIdProvider.getStrModuleId(moduleId); var paths = this._config.moduleIdToPaths(strModuleId); - if (this._env.isNode && strModuleId.indexOf('/') === -1) { + var scopedPackageRegex = /^@[^\/]+\/[^\/]+$/; // matches @scope/package-name + if (this._env.isNode && (strModuleId.indexOf('/') === -1 || scopedPackageRegex.test(strModuleId))) { paths.push('node|' + strModuleId); } var lastPathIndex = -1; @@ -1649,6 +1661,9 @@ var AMDLoader; RequireFunc.getStats = function () { return moduleManager.getLoaderEvents(); }; + RequireFunc.define = function () { + return DefineFunc.apply(null, arguments); + }; function init() { if (typeof AMDLoader.global.require !== 'undefined' || typeof require !== 'undefined') { var _nodeRequire_1 = (AMDLoader.global.require || require); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8a49ab1c4d4..ad533ea43d5 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -25,13 +25,6 @@ declare namespace monaco { dispose(): void; } - export enum Severity { - Ignore = 0, - Info = 1, - Warning = 2, - Error = 3, - } - export enum MarkerTag { Unnecessary = 1, } @@ -45,31 +38,13 @@ declare namespace monaco { - - export type TValueCallback = (value: T | PromiseLike) => void; - - export type ProgressCallback = (progress: TProgress) => void; - - - export class Promise { - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason: any) => void, - progress: (progress: TProgress) => void) => void, - oncancel?: () => void); + export class Promise { + constructor(executor: (resolve: (value: T | PromiseLike) => void, reject: (reason: any) => void) => void); public then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, - onprogress?: (progress: TProgress) => void): Promise; + onrejected?: ((reason: any) => TResult2 | PromiseLike) | null): Promise; - public done( - onfulfilled?: (value: T) => void, - onrejected?: (reason: any) => void, - onprogress?: (progress: TProgress) => void): void; - - public cancel(): void; public static as(value: null): Promise; public static as(value: undefined): Promise; @@ -77,13 +52,8 @@ declare namespace monaco { public static as>(value: SomePromise): SomePromise; public static as(value: T): Promise; - public static is(value: any): value is PromiseLike; - - public static timeout(delay: number): Promise; - public static join(promises: [T1 | PromiseLike, T2 | PromiseLike]): Promise<[T1, T2]>; public static join(promises: (T | PromiseLike)[]): Promise; - public static join(promises: { [n: string]: T | PromiseLike }): Promise<{ [n: string]: T }>; public static any(promises: (T | PromiseLike)[]): Promise<{ key: string; value: Promise; }>; @@ -108,7 +78,7 @@ declare namespace monaco { } /** * Uniform Resource Identifier (Uri) http://tools.ietf.org/html/rfc3986. - * This class is a simple parser which creates the basic component paths + * This class is a simple parser which creates the basic component parts * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation * and encoding. * @@ -119,8 +89,6 @@ declare namespace monaco { * | _____________________|__ * / \ / \ * urn:example:animal:ferret:nose - * - * */ export class Uri implements UriComponents { static isUri(thing: any): thing is Uri; @@ -148,28 +116,80 @@ declare namespace monaco { readonly fragment: string; /** * Returns a string representing the corresponding file system path of this Uri. - * Will handle UNC paths and normalize windows drive letters to lower-case. Also - * uses the platform specific path separator. Will *not* validate the path for - * invalid characters and semantics. Will *not* look at the scheme of this Uri. + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this Uri. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `Uri#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = Uri.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `Uri#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `Uri#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). */ readonly fsPath: string; with(change: { scheme?: string; - authority?: string; - path?: string; - query?: string; - fragment?: string; + authority?: string | null; + path?: string | null; + query?: string | null; + fragment?: string | null; }): Uri; + /** + * Creates a new Uri from a string, e.g. `http://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an Uri (see `Uri#toString`). + */ static parse(value: string): Uri; + /** + * Creates a new Uri from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `Uri#parse` and `Uri#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `Uri.file(path)` is **not the same as** + * `Uri.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = Uri.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + const bad = Uri.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` + * + * @param path A file system path (see `Uri#fsPath`) + */ static file(path: string): Uri; static from(components: { - scheme?: string; + scheme: string; authority?: string; path?: string; query?: string; fragment?: string; }): Uri; /** + * Creates a string presentation for this Uri. It's guardeed that calling + * `Uri.parse` with the result of this function creates an Uri which is equal + * to this Uri. + * + * * The result shall *not* be used for display purposes but for externalization or transport. + * * The result will be encoded using the percentage encoding and encoding happens mostly + * ignore the scheme-specific encoding rules. * * @param skipEncoding Do not encode the result, default is `false` */ @@ -822,7 +842,7 @@ declare namespace monaco.editor { /** * Change the language for a model. */ - export function setModelLanguage(model: ITextModel, language: string): void; + export function setModelLanguage(model: ITextModel, languageId: string): void; /** * Set the markers for a model. @@ -898,7 +918,7 @@ declare namespace monaco.editor { export function tokenize(text: string, languageId: string): Token[][]; /** - * Define a new theme. + * Define a new theme or updte an existing theme. */ export function defineTheme(themeName: string, themeData: IStandaloneThemeData): void; @@ -913,6 +933,7 @@ declare namespace monaco.editor { base: BuiltinTheme; inherit: boolean; rules: ITokenThemeRule[]; + encodedTokensColors?: string[]; colors: IColors; } @@ -1014,7 +1035,7 @@ declare namespace monaco.editor { /** * The initial model associated with this code editor. */ - model?: ITextModel; + model?: ITextModel | null; /** * The initial value of the auto created model in the editor. * To not create automatically a model, use `model: null`. @@ -1093,7 +1114,7 @@ declare namespace monaco.editor { endLineNumber: number; endColumn: number; relatedInformation?: IRelatedInformation[]; - customTags?: MarkerTag[]; + tags?: MarkerTag[]; } /** @@ -1109,7 +1130,7 @@ declare namespace monaco.editor { endLineNumber: number; endColumn: number; relatedInformation?: IRelatedInformation[]; - customTags?: MarkerTag[]; + tags?: MarkerTag[]; } /** @@ -1629,32 +1650,12 @@ declare namespace monaco.editor { /** * Get the word under or besides `position`. * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. * @return The word under or besides `position`. Might be null. */ getWordAtPosition(position: IPosition): IWordAtPosition; /** * Get the word under or besides `position` trimmed to `position`.column * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. - * @return The word under or besides `position`. Will never be null. - */ - getWordUntilPosition(position: IPosition): IWordAtPosition; - /** - * Get the language associated with this model. - */ - getModeId(): string; - /** - * Get the word under or besides `position`. - * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. - * @return The word under or besides `position`. Might be null. - */ - getWordAtPosition(position: IPosition): IWordAtPosition; - /** - * Get the word under or besides `position` trimmed to `position`.column - * @param position The position to look for a word. - * @param skipSyntaxTokens Ignore syntax tokens, as identified by the mode. * @return The word under or besides `position`. Will never be null. */ getWordUntilPosition(position: IPosition): IWordAtPosition; @@ -2153,7 +2154,7 @@ declare namespace monaco.editor { /** * Gets the current model attached to this editor. */ - getModel(): IEditorModel; + getModel(): IEditorModel | null; /** * Sets the current model attached to this editor. * If the previous model was created by the editor via the value key in the options @@ -2162,7 +2163,7 @@ declare namespace monaco.editor { * will not be destroyed. * It is safe to call setModel(null) to simply detach the current model from the editor. */ - setModel(model: IEditorModel): void; + setModel(model: IEditorModel | null): void; } /** @@ -2445,13 +2446,23 @@ declare namespace monaco.editor { autoFindInSelection: boolean; } + /** + * Configuration options for auto closing quotes and brackets + */ + export type EditorAutoClosingStrategy = 'always' | 'languageDefined' | 'beforeWhitespace' | 'never'; + + /** + * Configuration options for auto wrapping quotes and brackets + */ + export type EditorAutoSurroundStrategy = 'languageDefined' | 'quotes' | 'brackets' | 'never'; + /** * Configuration options for editor minimap */ export interface IEditorMinimapOptions { /** * Enable the rendering of the minimap. - * Defaults to false. + * Defaults to true. */ enabled?: boolean; /** @@ -2487,6 +2498,54 @@ declare namespace monaco.editor { enabled?: boolean; } + /** + * Configuration options for editor hover + */ + export interface IEditorHoverOptions { + /** + * Enable the hover. + * Defaults to true. + */ + enabled?: boolean; + /** + * Delay for showing the hover. + * Defaults to 300. + */ + delay?: number; + /** + * Is the hover sticky such that it can be clicked and its contents selected? + * Defaults to true. + */ + sticky?: boolean; + } + + /** + * Configuration options for parameter hints + */ + export interface IEditorParameterHintOptions { + /** + * Enable parameter hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Enable cycling of parameter hints. + * Defaults to false. + */ + cycle?: boolean; + } + + export interface ISuggestOptions { + /** + * Enable graceful matching. Defaults to true. + */ + filterGraceful?: boolean; + /** + * Prevent quick suggestions when a snippet is active. Defaults to true. + */ + snippetsPreventQuickSuggestions?: boolean; + } + /** * Configuration map for codeActionsOnSave */ @@ -2705,10 +2764,9 @@ declare namespace monaco.editor { */ stopRenderingLineAfter?: number; /** - * Enable hover. - * Defaults to true. + * Configure the editor's hover. */ - hover?: boolean; + hover?: IEditorHoverOptions; /** * Enable detecting links and making them clickable. * Defaults to true. @@ -2743,6 +2801,10 @@ declare namespace monaco.editor { * Defaults to 'auto'. It is best to leave this to 'auto'. */ accessibilitySupport?: 'auto' | 'off' | 'on'; + /** + * Suggest options. + */ + suggest?: ISuggestOptions; /** * Enable quick suggestions (shadow suggestions) * Defaults to true. @@ -2758,19 +2820,29 @@ declare namespace monaco.editor { */ quickSuggestionsDelay?: number; /** - * Enables parameter hints + * Parameter hint options. */ - parameterHints?: boolean; + parameterHints?: IEditorParameterHintOptions; /** * Render icons in suggestions box. * Defaults to true. */ iconsInSuggestions?: boolean; /** - * Enable auto closing brackets. - * Defaults to true. + * Options for auto closing brackets. + * Defaults to language defined behavior. */ - autoClosingBrackets?: boolean; + autoClosingBrackets?: EditorAutoClosingStrategy; + /** + * Options for auto closing quotes. + * Defaults to language defined behavior. + */ + autoClosingQuotes?: EditorAutoClosingStrategy; + /** + * Options for auto surrounding. + * Defaults to always allowing auto surrounding. + */ + autoSurround?: EditorAutoSurroundStrategy; /** * Enable auto indentation adjustment. * Defaults to false. @@ -2814,6 +2886,10 @@ declare namespace monaco.editor { * Copying without a selection copies the current line. */ emptySelectionClipboard?: boolean; + /** + * Syntax highlighting is copied. + */ + copyWithSyntaxHighlighting?: boolean; /** * Enable word based suggestions. Defaults to 'true' */ @@ -2891,9 +2967,14 @@ declare namespace monaco.editor { renderControlCharacters?: boolean; /** * Enable rendering of indent guides. - * Defaults to false. + * Defaults to true. */ renderIndentGuides?: boolean; + /** + * Enable highlighting of the active indent guide. + * Defaults to true. + */ + highlightActiveIndentGuide?: boolean; /** * Enable rendering of current line highlight. * Defaults to all. @@ -2923,6 +3004,10 @@ declare namespace monaco.editor { * The letter spacing */ letterSpacing?: number; + /** + * Controls fading out of unused variables. + */ + showUnused?: boolean; } /** @@ -3074,6 +3159,23 @@ declare namespace monaco.editor { readonly autoFindInSelection: boolean; } + export interface InternalEditorHoverOptions { + readonly enabled: boolean; + readonly delay: number; + readonly sticky: boolean; + } + + export interface InternalSuggestOptions { + readonly filterGraceful: boolean; + readonly snippets: 'top' | 'bottom' | 'inline' | 'none'; + readonly snippetsPreventQuickSuggestions: boolean; + } + + export interface InternalParameterHintOptions { + readonly enabled: boolean; + readonly cycle: boolean; + } + export interface EditorWrappingInfo { readonly inDiffEditor: boolean; readonly isDominatedByLongLines: boolean; @@ -3120,6 +3222,7 @@ declare namespace monaco.editor { readonly renderControlCharacters: boolean; readonly fontLigatures: boolean; readonly renderIndentGuides: boolean; + readonly highlightActiveIndentGuide: boolean; readonly renderLineHighlight: 'none' | 'gutter' | 'line' | 'all'; readonly scrollbar: InternalEditorScrollbarOptions; readonly minimap: InternalEditorMinimapOptions; @@ -3128,7 +3231,7 @@ declare namespace monaco.editor { export interface EditorContribOptions { readonly selectionClipboard: boolean; - readonly hover: boolean; + readonly hover: InternalEditorHoverOptions; readonly links: boolean; readonly contextmenu: boolean; readonly quickSuggestions: boolean | { @@ -3137,18 +3240,18 @@ declare namespace monaco.editor { strings: boolean; }; readonly quickSuggestionsDelay: number; - readonly parameterHints: boolean; + readonly parameterHints: InternalParameterHintOptions; readonly iconsInSuggestions: boolean; readonly formatOnType: boolean; readonly formatOnPaste: boolean; readonly suggestOnTriggerCharacters: boolean; readonly acceptSuggestionOnEnter: 'on' | 'smart' | 'off'; readonly acceptSuggestionOnCommitCharacter: boolean; - readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; readonly wordBasedSuggestions: boolean; readonly suggestSelection: 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'; readonly suggestFontSize: number; readonly suggestLineHeight: number; + readonly suggest: InternalSuggestOptions; readonly selectionHighlight: boolean; readonly occurrencesHighlight: boolean; readonly codeLens: boolean; @@ -3175,13 +3278,17 @@ declare namespace monaco.editor { readonly readOnly: boolean; readonly multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'; readonly multiCursorMergeOverlapping: boolean; + readonly showUnused: boolean; readonly wordSeparators: string; - readonly autoClosingBrackets: boolean; + readonly autoClosingBrackets: EditorAutoClosingStrategy; + readonly autoClosingQuotes: EditorAutoClosingStrategy; + readonly autoSurround: EditorAutoSurroundStrategy; readonly autoIndent: boolean; readonly useTabStops: boolean; readonly tabFocusMode: boolean; readonly dragAndDrop: boolean; readonly emptySelectionClipboard: boolean; + readonly copyWithSyntaxHighlighting: boolean; readonly layoutInfo: EditorLayoutInfo; readonly fontInfo: FontInfo; readonly viewInfo: InternalEditorViewOptions; @@ -3315,11 +3422,14 @@ declare namespace monaco.editor { readonly multiCursorMergeOverlapping: boolean; readonly wordSeparators: boolean; readonly autoClosingBrackets: boolean; + readonly autoClosingQuotes: boolean; + readonly autoSurround: boolean; readonly autoIndent: boolean; readonly useTabStops: boolean; readonly tabFocusMode: boolean; readonly dragAndDrop: boolean; readonly emptySelectionClipboard: boolean; + readonly copyWithSyntaxHighlighting: boolean; readonly layoutInfo: boolean; readonly fontInfo: boolean; readonly viewInfo: boolean; @@ -3678,6 +3788,14 @@ declare namespace monaco.editor { * @event */ onDidBlurEditorWidget(listener: () => void): IDisposable; + /** + * An event emitted after composition has started. + */ + onCompositionStart(listener: () => void): IDisposable; + /** + * An event emitted after composition has ended. + */ + onCompositionEnd(listener: () => void): IDisposable; /** * An event emitted on a "mouseup". * @event @@ -3812,9 +3930,9 @@ declare namespace monaco.editor { * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. * @param source The source of the call. * @param edits The edits to execute. - * @param endCursoState Cursor state after the edits were applied. + * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursoState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; /** * Execute multiple (concommitent) commands on the editor. * @param source The source of the call. @@ -3978,6 +4096,7 @@ declare namespace monaco.editor { readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; readonly typicalFullwidthCharacterWidth: number; + readonly canUseHalfwidthRightwardsArrow: boolean; readonly spaceWidth: number; readonly maxDigitWidth: number; } @@ -4009,6 +4128,8 @@ declare namespace monaco.languages { */ export function getLanguages(): ILanguageExtensionPoint[]; + export function getEncodedLanguageId(languageId: string): number; + /** * An event emitted when a language is first time needed (e.g. a model has it set). * @event @@ -4043,6 +4164,38 @@ declare namespace monaco.languages { endState: IState; } + /** + * The result of a line tokenization. + */ + export interface IEncodedLineTokens { + /** + * The tokens on the line in a binary, encoded format. Each token occupies two array indices. For token i: + * - at offset 2*i => startIndex + * - at offset 2*i + 1 => metadata + * Meta data is in binary format: + * - ------------------------------------------- + * 3322 2222 2222 1111 1111 1100 0000 0000 + * 1098 7654 3210 9876 5432 1098 7654 3210 + * - ------------------------------------------- + * bbbb bbbb bfff ffff ffFF FTTT LLLL LLLL + * - ------------------------------------------- + * - L = EncodedLanguageId (8 bits): Use `getEncodedLanguageId` to get the encoded ID of a language. + * - T = StandardTokenType (3 bits): Other = 0, Comment = 1, String = 2, RegEx = 4. + * - F = FontStyle (3 bits): None = 0, Italic = 1, Bold = 2, Underline = 4. + * - f = foreground ColorId (9 bits) + * - b = background ColorId (9 bits) + * - The color value for each colorId is defined in IStandaloneThemeData.customTokenColors: + * e.g colorId = 1 is stored in IStandaloneThemeData.customTokenColors[1]. Color id = 0 means no color, + * id = 1 is for the default foreground color, id = 2 for the default background. + */ + tokens: Uint32Array; + /** + * The tokenization end state. + * A pointer will be held to this and the object should not be modified by the tokenizer after the pointer is returned. + */ + endState: IState; + } + /** * A "manual" provider of tokens. */ @@ -4057,10 +4210,24 @@ declare namespace monaco.languages { tokenize(line: string, state: IState): ILineTokens; } + /** + * A "manual" provider of tokens, returning tokens in a binary form. + */ + export interface EncodedTokensProvider { + /** + * The initial state of a language. Will be the state passed in to tokenize the first line. + */ + getInitialState(): IState; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenizeEncoded(line: string, state: IState): IEncodedLineTokens; + } + /** * Set the tokens provider for a language (manual implementation). */ - export function setTokensProvider(languageId: string, provider: TokensProvider): IDisposable; + export function setTokensProvider(languageId: string, provider: TokensProvider | EncodedTokensProvider): IDisposable; /** * Set the tokens provider for a language (monarch implementation). @@ -4423,6 +4590,12 @@ declare namespace monaco.languages { * settings will be used. */ surroundingPairs?: IAutoClosingPair[]; + /** + * Defines what characters must be after the cursor for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. + * + * This is typically the set of characters which can not start an expression, such as whitespace, closing brackets, non-unary operators, etc. + */ + autoCloseBefore?: string; /** * The language's folding rules. */ @@ -4596,6 +4769,14 @@ declare namespace monaco.languages { equals(other: IState): boolean; } + /** + * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), + * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves + * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a + * thenable. + */ + export type ProviderResult = T | undefined | null | Thenable; + /** * A hover represents additional information for a symbol or word. Hovers are * rendered in a tooltip-like widget. @@ -4623,7 +4804,7 @@ declare namespace monaco.languages { * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: editor.ITextModel, position: Position, token: CancellationToken): Hover | Thenable; + provideHover(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4702,6 +4883,17 @@ declare namespace monaco.languages { activeParameter: number; } + export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3 + } + + export interface SignatureHelpContext { + triggerReason: SignatureHelpTriggerReason; + triggerCharacter?: string; + } + /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. @@ -4711,7 +4903,7 @@ declare namespace monaco.languages { /** * Provide help for the signature at the given position and document. */ - provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable; + provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; } /** @@ -4757,7 +4949,7 @@ declare namespace monaco.languages { * Provide a set of document highlights, like all occurrences of a variable or * all exit-points of a function. */ - provideDocumentHighlights(model: editor.ITextModel, position: Position, token: CancellationToken): DocumentHighlight[] | Thenable; + provideDocumentHighlights(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4779,7 +4971,7 @@ declare namespace monaco.languages { /** * Provide a set of project-wide references for the given position and document. */ - provideReferences(model: editor.ITextModel, position: Position, context: ReferenceContext, token: CancellationToken): Location[] | Thenable; + provideReferences(model: editor.ITextModel, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult; } /** @@ -4804,6 +4996,13 @@ declare namespace monaco.languages { */ export type Definition = Location | Location[]; + export interface DefinitionLink { + origin?: IRange; + uri: Uri; + range: IRange; + selectionRange?: IRange; + } + /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) @@ -4813,7 +5012,7 @@ declare namespace monaco.languages { /** * Provide the definition of the symbol at the given position and document. */ - provideDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4824,7 +5023,7 @@ declare namespace monaco.languages { /** * Provide the implementation of the symbol at the given position and document. */ - provideImplementation(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideImplementation(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4835,7 +5034,7 @@ declare namespace monaco.languages { /** * Provide the type definition of the symbol at the given position and document. */ - provideTypeDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): Definition | Thenable; + provideTypeDefinition(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } /** @@ -4870,36 +5069,14 @@ declare namespace monaco.languages { TypeParameter = 25 } - /** - * Represents information about programming constructs like variables, classes, - * interfaces etc. - */ - export interface SymbolInformation { - /** - * The name of this symbol. - */ + export interface DocumentSymbol { name: string; - /** - * The detail of this symbol. - */ - detail?: string; - /** - * The name of the symbol containing this symbol. - */ - containerName?: string; - /** - * The kind of this symbol. - */ + detail: string; kind: SymbolKind; - /** - * The location of this symbol. - */ - location: Location; - /** - * The defining range of this symbol. - */ - definingRange: IRange; - children?: SymbolInformation[]; + containerName?: string; + range: IRange; + selectionRange: IRange; + children?: DocumentSymbol[]; } /** @@ -4907,11 +5084,11 @@ declare namespace monaco.languages { * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_goto-symbol)-feature. */ export interface DocumentSymbolProvider { - extensionId?: string; + displayName?: string; /** * Provide symbol information for the given document. */ - provideDocumentSymbols(model: editor.ITextModel, token: CancellationToken): SymbolInformation[] | Thenable; + provideDocumentSymbols(model: editor.ITextModel, token: CancellationToken): ProviderResult; } export interface TextEdit { @@ -4942,7 +5119,7 @@ declare namespace monaco.languages { /** * Provide formatting edits for a whole document. */ - provideDocumentFormattingEdits(model: editor.ITextModel, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideDocumentFormattingEdits(model: editor.ITextModel, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** @@ -4957,7 +5134,7 @@ declare namespace monaco.languages { * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. */ - provideDocumentRangeFormattingEdits(model: editor.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideDocumentRangeFormattingEdits(model: editor.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** @@ -4973,7 +5150,7 @@ declare namespace monaco.languages { * what range the position to expand to, like find the matching `{` * when `}` has been entered. */ - provideOnTypeFormattingEdits(model: editor.ITextModel, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): TextEdit[] | Thenable; + provideOnTypeFormattingEdits(model: editor.ITextModel, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult; } /** @@ -4988,8 +5165,8 @@ declare namespace monaco.languages { * A provider of links. */ export interface LinkProvider { - provideLinks(model: editor.ITextModel, token: CancellationToken): ILink[] | Thenable; - resolveLink?: (link: ILink, token: CancellationToken) => ILink | Thenable; + provideLinks(model: editor.ITextModel, token: CancellationToken): ProviderResult; + resolveLink?: (link: ILink, token: CancellationToken) => ProviderResult; } /** @@ -5057,11 +5234,11 @@ declare namespace monaco.languages { /** * Provides the color ranges for a specific model. */ - provideDocumentColors(model: editor.ITextModel, token: CancellationToken): IColorInformation[] | Thenable; + provideDocumentColors(model: editor.ITextModel, token: CancellationToken): ProviderResult; /** * Provide the string representations for a color. */ - provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; + provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): ProviderResult; } export interface FoldingContext { @@ -5074,16 +5251,16 @@ declare namespace monaco.languages { /** * Provides the color ranges for a specific model. */ - provideFoldingRanges(model: editor.ITextModel, context: FoldingContext, token: CancellationToken): FoldingRange[] | Thenable; + provideFoldingRanges(model: editor.ITextModel, context: FoldingContext, token: CancellationToken): ProviderResult; } export interface FoldingRange { /** - * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * The one-based start line of the range to fold. The folded area starts after the line's last character. */ start: number; /** - * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * The one-based end line of the range to fold. The folded area ends with the line's last character. */ end: number; /** @@ -5121,6 +5298,12 @@ declare namespace monaco.languages { export interface ResourceFileEdit { oldUri: Uri; newUri: Uri; + options: { + overwrite?: boolean; + ignoreIfNotExists?: boolean; + ignoreIfExists?: boolean; + recursive?: boolean; + }; } export interface ResourceTextEdit { @@ -5131,6 +5314,9 @@ declare namespace monaco.languages { export interface WorkspaceEdit { edits: Array; + } + + export interface Rejection { rejectReason?: string; } @@ -5140,8 +5326,8 @@ declare namespace monaco.languages { } export interface RenameProvider { - provideRenameEdits(model: editor.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; - resolveRenameLocation?(model: editor.ITextModel, position: Position, token: CancellationToken): RenameLocation | Thenable; + provideRenameEdits(model: editor.ITextModel, position: Position, newName: string, token: CancellationToken): ProviderResult; + resolveRenameLocation?(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } export interface Command { @@ -5151,76 +5337,6 @@ declare namespace monaco.languages { arguments?: any[]; } - export interface CommentInfo { - owner: number; - threads: CommentThread[]; - commentingRanges?: IRange[]; - reply?: Command; - } - - export enum CommentThreadCollapsibleState { - /** - * Determines an item is collapsed - */ - Collapsed = 0, - /** - * Determines an item is expanded - */ - Expanded = 1 - } - - export interface CommentThread { - threadId: string; - resource: string; - range: IRange; - comments: Comment[]; - collapsibleState?: CommentThreadCollapsibleState; - reply?: Command; - } - - export interface NewCommentAction { - ranges: IRange[]; - actions: Command[]; - } - - export interface Comment { - readonly commentId: string; - readonly body: IMarkdownString; - readonly userName: string; - readonly gravatar: string; - readonly command?: Command; - } - - export interface CommentThreadChangedEvent { - readonly owner: number; - /** - * Added comment threads. - */ - readonly added: CommentThread[]; - /** - * Removed comment threads. - */ - readonly removed: CommentThread[]; - /** - * Changed comment threads. - */ - readonly changed: CommentThread[]; - } - - export interface DocumentCommentProvider { - provideDocumentComments(resource: Uri, token: CancellationToken): Promise; - createNewCommentThread(resource: Uri, range: Range, text: string, token: CancellationToken): Promise; - replyToCommentThread(resource: Uri, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; - onDidChangeCommentThreads(): IEvent; - } - - export interface WorkspaceCommentProvider { - provideWorkspaceComments(token: CancellationToken): Promise; - createNewCommentThread(resource: Uri, range: Range, text: string, token: CancellationToken): Promise; - replyToCommentThread(resource: Uri, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; - onDidChangeCommentThreads(): IEvent; - } - export interface ICodeLensSymbol { range: IRange; id?: string; @@ -5229,8 +5345,8 @@ declare namespace monaco.languages { export interface CodeLensProvider { onDidChange?: IEvent; - provideCodeLenses(model: editor.ITextModel, token: CancellationToken): ICodeLensSymbol[] | Thenable; - resolveCodeLens?(model: editor.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ICodeLensSymbol | Thenable; + provideCodeLenses(model: editor.ITextModel, token: CancellationToken): ProviderResult; + resolveCodeLens?(model: editor.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ProviderResult; } export interface ILanguageExtensionPoint { @@ -5241,7 +5357,7 @@ declare namespace monaco.languages { firstLine?: string; aliases?: string[]; mimetypes?: string[]; - configuration?: string; + configuration?: Uri; } /** * A Monarch language definition @@ -5272,7 +5388,7 @@ declare namespace monaco.languages { /** * attach this to every token class (by default '.' + name) */ - tokenPostfix: string; + tokenPostfix?: string; } /** @@ -5280,7 +5396,11 @@ declare namespace monaco.languages { * shorthands: [reg,act] == { regex: reg, action: act} * and : [reg,act,nxt] == { regex: reg, action: act{ next: nxt }} */ - export interface IMonarchLanguageRule { + export type IShortMonarchLanguageRule1 = [RegExp, IMonarchLanguageAction]; + + export type IShortMonarchLanguageRule2 = [RegExp, IMonarchLanguageAction, string]; + + export interface IExpandedMonarchLanguageRule { /** * match tokens */ @@ -5295,12 +5415,16 @@ declare namespace monaco.languages { include?: string; } + export type IMonarchLanguageRule = IShortMonarchLanguageRule1 | IShortMonarchLanguageRule2 | IExpandedMonarchLanguageRule; + /** * An action is either an array of actions... * ... or a case statement with guards... * ... or a basic action with a token value. */ - export interface IMonarchLanguageAction { + export type IShortMonarchLanguageAction = string; + + export interface IExpandedMonarchLanguageAction { /** * array of actions for each parenthesized match group */ @@ -5339,6 +5463,8 @@ declare namespace monaco.languages { log?: string; } + export type IMonarchLanguageAction = IShortMonarchLanguageAction | IExpandedMonarchLanguageAction | IShortMonarchLanguageAction[] | IExpandedMonarchLanguageAction[]; + /** * This interface can be shortened as an array, ie. ['{','}','delimiter.curly'] */ diff --git a/src/vs/nls.build.js b/src/vs/nls.build.js index ece108ed635..37077e8c32f 100644 --- a/src/vs/nls.build.js +++ b/src/vs/nls.build.js @@ -17,166 +17,166 @@ var _nlsPluginGlobal = this; var NLSBuildLoaderPlugin; (function (NLSBuildLoaderPlugin) { - var global = _nlsPluginGlobal || {}; - var Resources = global.Plugin && global.Plugin.Resources ? global.Plugin.Resources : undefined; - var IS_PSEUDO = (global && global.document && global.document.location && global.document.location.hash.indexOf('pseudo=true') >= 0); - function _format(message, args) { - var result; - if (args.length === 0) { - result = message; - } - else { - result = message.replace(/\{(\d+)\}/g, function (match, rest) { - var index = rest[0]; - return typeof args[index] !== 'undefined' ? args[index] : match; - }); - } - if (IS_PSEUDO) { - // FF3B and FF3D is the Unicode zenkaku representation for [ and ] - result = '\uFF3B' + result.replace(/[aouei]/g, '$&$&') + '\uFF3D'; - } - return result; - } - function findLanguageForModule(config, name) { - var result = config[name]; - if (result) - return result; - result = config['*']; - if (result) - return result; - return null; - } - function localize(data, message) { - var args = []; - for (var _i = 0; _i < (arguments.length - 2); _i++) { - args[_i] = arguments[_i + 2]; - } - return _format(message, args); - } - function createScopedLocalize(scope) { - return function (idx, defaultValue) { - var restArgs = Array.prototype.slice.call(arguments, 2); - return _format(scope[idx], restArgs); - }; - } - var NLSPlugin = (function () { - function NLSPlugin() { - this.localize = localize; - } - NLSPlugin.prototype.setPseudoTranslation = function (value) { - IS_PSEUDO = value; - }; - NLSPlugin.prototype.create = function (key, data) { - return { - localize: createScopedLocalize(data[key]) - }; - }; - NLSPlugin.prototype.load = function (name, req, load, config) { - config = config || {}; - if (!name || name.length === 0) { - load({ - localize: localize - }); - } - else { - var suffix = void 0; - if (Resources && Resources.getString) { - suffix = '.nls.keys'; - req([name + suffix], function (keyMap) { - load({ - localize: function (moduleKey, index) { - if (!keyMap[moduleKey]) - return 'NLS error: unknown key ' + moduleKey; - var mk = keyMap[moduleKey].keys; - if (index >= mk.length) - return 'NLS error unknow index ' + index; - var subKey = mk[index]; - var args = []; - args[0] = moduleKey + '_' + subKey; - for (var _i = 0; _i < (arguments.length - 2); _i++) { - args[_i + 1] = arguments[_i + 2]; - } - return Resources.getString.apply(Resources, args); - } - }); - }); - } - else { - if (config.isBuild) { - req([name + '.nls', name + '.nls.keys'], function (messages, keys) { - NLSPlugin.BUILD_MAP[name] = messages; - NLSPlugin.BUILD_MAP_KEYS[name] = keys; - load(messages); - }); - } - else { - var pluginConfig = config['vs/nls'] || {}; - var language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null; - suffix = '.nls'; - if (language !== null && language !== NLSPlugin.DEFAULT_TAG) { - suffix = suffix + '.' + language; - } - req([name + suffix], function (messages) { - if (Array.isArray(messages)) { - messages.localize = createScopedLocalize(messages); - } - else { - messages.localize = createScopedLocalize(messages[name]); - } - load(messages); - }); - } - } - } - }; - NLSPlugin.prototype._getEntryPointsMap = function () { - global.nlsPluginEntryPoints = global.nlsPluginEntryPoints || {}; - return global.nlsPluginEntryPoints; - }; - NLSPlugin.prototype.write = function (pluginName, moduleName, write) { - // getEntryPoint is a Monaco extension to r.js - var entryPoint = write.getEntryPoint(); - // r.js destroys the context of this plugin between calling 'write' and 'writeFile' - // so the only option at this point is to leak the data to a global - var entryPointsMap = this._getEntryPointsMap(); - entryPointsMap[entryPoint] = entryPointsMap[entryPoint] || []; - entryPointsMap[entryPoint].push(moduleName); - if (moduleName !== entryPoint) { - write.asModule(pluginName + '!' + moduleName, 'define([\'vs/nls\', \'vs/nls!' + entryPoint + '\'], function(nls, data) { return nls.create("' + moduleName + '", data); });'); - } - }; - NLSPlugin.prototype.writeFile = function (pluginName, moduleName, req, write, config) { - var entryPointsMap = this._getEntryPointsMap(); - if (entryPointsMap.hasOwnProperty(moduleName)) { - var fileName = req.toUrl(moduleName + '.nls.js'); - var contents = [ - '/*---------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' *--------------------------------------------------------*/' - ], entries = entryPointsMap[moduleName]; - var data = {}; - for (var i = 0; i < entries.length; i++) { - data[entries[i]] = NLSPlugin.BUILD_MAP[entries[i]]; - } - contents.push('define("' + moduleName + '.nls", ' + JSON.stringify(data, null, '\t') + ');'); - write(fileName, contents.join('\r\n')); - } - }; - NLSPlugin.prototype.finishBuild = function (write) { - write('nls.metadata.json', JSON.stringify({ - keys: NLSPlugin.BUILD_MAP_KEYS, - messages: NLSPlugin.BUILD_MAP, - bundles: this._getEntryPointsMap() - }, null, '\t')); - }; - ; - return NLSPlugin; - }()); - NLSPlugin.DEFAULT_TAG = 'i-default'; - NLSPlugin.BUILD_MAP = {}; - NLSPlugin.BUILD_MAP_KEYS = {}; - NLSBuildLoaderPlugin.NLSPlugin = NLSPlugin; - (function () { - define('vs/nls', new NLSPlugin()); - })(); + var global = _nlsPluginGlobal || {}; + var Resources = global.Plugin && global.Plugin.Resources ? global.Plugin.Resources : undefined; + var IS_PSEUDO = (global && global.document && global.document.location && global.document.location.hash.indexOf('pseudo=true') >= 0); + function _format(message, args) { + var result; + if (args.length === 0) { + result = message; + } + else { + result = message.replace(/\{(\d+)\}/g, function (match, rest) { + var index = rest[0]; + return typeof args[index] !== 'undefined' ? args[index] : match; + }); + } + if (IS_PSEUDO) { + // FF3B and FF3D is the Unicode zenkaku representation for [ and ] + result = '\uFF3B' + result.replace(/[aouei]/g, '$&$&') + '\uFF3D'; + } + return result; + } + function findLanguageForModule(config, name) { + var result = config[name]; + if (result) + return result; + result = config['*']; + if (result) + return result; + return null; + } + function localize(data, message) { + var args = []; + for (var _i = 0; _i < (arguments.length - 2); _i++) { + args[_i] = arguments[_i + 2]; + } + return _format(message, args); + } + function createScopedLocalize(scope) { + return function (idx, defaultValue) { + var restArgs = Array.prototype.slice.call(arguments, 2); + return _format(scope[idx], restArgs); + }; + } + var NLSPlugin = /** @class */ (function () { + function NLSPlugin() { + this.localize = localize; + } + NLSPlugin.prototype.setPseudoTranslation = function (value) { + IS_PSEUDO = value; + }; + NLSPlugin.prototype.create = function (key, data) { + return { + localize: createScopedLocalize(data[key]) + }; + }; + NLSPlugin.prototype.load = function (name, req, load, config) { + config = config || {}; + if (!name || name.length === 0) { + load({ + localize: localize + }); + } + else { + var suffix = void 0; + if (Resources && Resources.getString) { + suffix = '.nls.keys'; + req([name + suffix], function (keyMap) { + load({ + localize: function (moduleKey, index) { + if (!keyMap[moduleKey]) + return 'NLS error: unknown key ' + moduleKey; + var mk = keyMap[moduleKey].keys; + if (index >= mk.length) + return 'NLS error unknow index ' + index; + var subKey = mk[index]; + var args = []; + args[0] = moduleKey + '_' + subKey; + for (var _i = 0; _i < (arguments.length - 2); _i++) { + args[_i + 1] = arguments[_i + 2]; + } + return Resources.getString.apply(Resources, args); + } + }); + }); + } + else { + if (config.isBuild) { + req([name + '.nls', name + '.nls.keys'], function (messages, keys) { + NLSPlugin.BUILD_MAP[name] = messages; + NLSPlugin.BUILD_MAP_KEYS[name] = keys; + load(messages); + }); + } + else { + var pluginConfig = config['vs/nls'] || {}; + var language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null; + suffix = '.nls'; + if (language !== null && language !== NLSPlugin.DEFAULT_TAG) { + suffix = suffix + '.' + language; + } + req([name + suffix], function (messages) { + if (Array.isArray(messages)) { + messages.localize = createScopedLocalize(messages); + } + else { + messages.localize = createScopedLocalize(messages[name]); + } + load(messages); + }); + } + } + } + }; + NLSPlugin.prototype._getEntryPointsMap = function () { + global.nlsPluginEntryPoints = global.nlsPluginEntryPoints || {}; + return global.nlsPluginEntryPoints; + }; + NLSPlugin.prototype.write = function (pluginName, moduleName, write) { + // getEntryPoint is a Monaco extension to r.js + var entryPoint = write.getEntryPoint(); + // r.js destroys the context of this plugin between calling 'write' and 'writeFile' + // so the only option at this point is to leak the data to a global + var entryPointsMap = this._getEntryPointsMap(); + entryPointsMap[entryPoint] = entryPointsMap[entryPoint] || []; + entryPointsMap[entryPoint].push(moduleName); + if (moduleName !== entryPoint) { + write.asModule(pluginName + '!' + moduleName, 'define([\'vs/nls\', \'vs/nls!' + entryPoint + '\'], function(nls, data) { return nls.create("' + moduleName + '", data); });'); + } + }; + NLSPlugin.prototype.writeFile = function (pluginName, moduleName, req, write, config) { + var entryPointsMap = this._getEntryPointsMap(); + if (entryPointsMap.hasOwnProperty(moduleName)) { + var fileName = req.toUrl(moduleName + '.nls.js'); + var contents = [ + '/*---------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' *--------------------------------------------------------*/' + ], entries = entryPointsMap[moduleName]; + var data = {}; + for (var i = 0; i < entries.length; i++) { + data[entries[i]] = NLSPlugin.BUILD_MAP[entries[i]]; + } + contents.push('define("' + moduleName + '.nls", ' + JSON.stringify(data, null, '\t') + ');'); + write(fileName, contents.join('\r\n')); + } + }; + NLSPlugin.prototype.finishBuild = function (write) { + write('nls.metadata.json', JSON.stringify({ + keys: NLSPlugin.BUILD_MAP_KEYS, + messages: NLSPlugin.BUILD_MAP, + bundles: this._getEntryPointsMap() + }, null, '\t')); + }; + ; + NLSPlugin.DEFAULT_TAG = 'i-default'; + NLSPlugin.BUILD_MAP = {}; + NLSPlugin.BUILD_MAP_KEYS = {}; + return NLSPlugin; + }()); + NLSBuildLoaderPlugin.NLSPlugin = NLSPlugin; + (function () { + define('vs/nls', new NLSPlugin()); + })(); })(NLSBuildLoaderPlugin || (NLSBuildLoaderPlugin = {})); diff --git a/src/vs/nls.d.ts b/src/vs/nls.d.ts index eae07d0b215..38bbd076068 100644 --- a/src/vs/nls.d.ts +++ b/src/vs/nls.d.ts @@ -8,5 +8,5 @@ export interface ILocalizeInfo { comment: string[]; } -export declare function localize(info: ILocalizeInfo, message: string, ...args: any[]): string; -export declare function localize(key: string, message: string, ...args: any[]): string; +export declare function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string; +export declare function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string; diff --git a/src/vs/nls.js b/src/vs/nls.js index 79b64a22484..8e178e8d0ea 100644 --- a/src/vs/nls.js +++ b/src/vs/nls.js @@ -46,7 +46,15 @@ var NLSLoaderPlugin; else { result = message.replace(/\{(\d+)\}/g, function (match, rest) { var index = rest[0]; - return typeof args[index] !== 'undefined' ? args[index] : match; + var arg = args[index]; + var result = match; + if (typeof arg === 'string') { + result = arg; + } + else if (typeof arg === 'number' || typeof arg === 'boolean' || arg === void 0 || arg === null) { + result = String(arg); + } + return result; }); } if (env.isPseudo) { diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index c79984b7d63..a4ef78d6f05 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -5,20 +5,19 @@ 'use strict'; -import { localize } from 'vs/nls'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IMenu, MenuItemAction, IMenuActionOptions, ICommandAction } from 'vs/platform/actions/common/actions'; -import { IAction } from 'vs/base/common/actions'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { addClasses, createCSSRule, removeClasses } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; +import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import { createCSSRule } from 'vs/base/browser/dom'; -import URI from 'vs/base/common/uri'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isLinux, isWindows } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isWindows, isLinux } from 'vs/base/common/platform'; // The alternative key on all platforms is alt. On windows we also support shift as an alternative key #44136 class AlternativeKeyEmitter extends Emitter { @@ -92,40 +91,17 @@ export function fillInActionBarActions(menu: IMenu, options: IMenuActionOptions, fillInActions(groups, target, false, isPrimaryGroup); } -function fillInActions(groups: [string, MenuItemAction[]][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, getAlternativeActions, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { +function fillInActions(groups: [string, (MenuItemAction | SubmenuItemAction)[]][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, getAlternativeActions, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { for (let tuple of groups) { let [group, actions] = tuple; if (getAlternativeActions) { - actions = actions.map(a => !!a.alt ? a.alt : a); + actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } if (isPrimaryGroup(group)) { + const to = Array.isArray(target) ? target : target.primary; - const head = Array.isArray(target) ? target : target.primary; - - // split contributed actions at the point where order - // changes form lt zero to gte - let pivot = 0; - for (; pivot < actions.length; pivot++) { - if ((actions[pivot]).order >= 0) { - break; - } - } - // prepend contributed actions with order lte zero - head.unshift(...actions.slice(0, pivot)); - - // find the first separator which marks the end of the - // navigation group - might be the whole array length - let sep = 0; - while (sep < head.length) { - if (head[sep] instanceof Separator) { - break; - } - sep++; - } - // append contributed actions with order gt zero - head.splice(sep, 0, ...actions.slice(pivot)); - + to.unshift(...actions); } else { const to = Array.isArray(target) ? target : target.secondary; @@ -161,7 +137,7 @@ export class MenuItemActionItem extends ActionItem { @INotificationService protected _notificationService: INotificationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService ) { - super(undefined, _action, { icon: !!(_action.class || _action.item.iconPath), label: !_action.class && !_action.item.iconPath }); + super(undefined, _action, { icon: !!(_action.class || _action.item.iconLocation), label: !_action.class && !_action.item.iconLocation }); } protected get _commandAction(): IAction { @@ -178,40 +154,52 @@ export class MenuItemActionItem extends ActionItem { } this.actionRunner.run(this._commandAction) - .done(undefined, err => this._notificationService.error(err)); + .then(undefined, err => this._notificationService.error(err)); } render(container: HTMLElement): void { super.render(container); this._updateItemClass(this._action.item); + + let mouseOver = false; const alternativeKeyEmitter = AlternativeKeyEmitter.getInstance(this._contextMenuService); let alternativeKeyDown = alternativeKeyEmitter.isPressed; const updateAltState = () => { - if (alternativeKeyDown !== this._wantsAltCommand) { - this._wantsAltCommand = alternativeKeyDown; - this._updateLabel(); - this._updateTooltip(); - this._updateClass(); + const wantsAltCommand = mouseOver && alternativeKeyDown; + if (wantsAltCommand !== this._wantsAltCommand) { + this._wantsAltCommand = wantsAltCommand; + this.updateLabel(); + this.updateTooltip(); + this.updateClass(); } }; - updateAltState(); - this._callOnDispose.push(alternativeKeyEmitter.event(value => { + this._register(alternativeKeyEmitter.event(value => { alternativeKeyDown = value; updateAltState(); })); + + this._register(domEvent(container, 'mouseleave')(_ => { + mouseOver = false; + updateAltState(); + })); + + this._register(domEvent(container, 'mouseenter')(e => { + mouseOver = true; + updateAltState(); + })); } - _updateLabel(): void { + updateLabel(): void { if (this.options.label) { - this.$e.text(this._commandAction.label); + this.label.textContent = this._commandAction.label; } } - _updateTooltip(): void { - const element = this.$e.getHTMLElement(); + updateTooltip(): void { + const element = this.label; const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id); const keybindingLabel = keybinding && keybinding.getLabel(); @@ -220,7 +208,7 @@ export class MenuItemActionItem extends ActionItem { : this._commandAction.label; } - _updateClass(): void { + updateClass(): void { if (this.options.icon) { if (this._commandAction !== this._action) { this._updateItemClass(this._action.alt.item); @@ -234,20 +222,22 @@ export class MenuItemActionItem extends ActionItem { dispose(this._itemClassDispose); this._itemClassDispose = undefined; - if (item.iconPath) { + if (item.iconLocation) { let iconClass: string; - if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) { - iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark); + const iconPathMapKey = item.iconLocation.dark.toString(); + + if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { + iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey); } else { iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.light || item.iconPath.dark).toString()}")`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.dark).toString()}")`); - MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath.dark, iconClass); + createCSSRule(`.icon.${iconClass}`, `background-image: url("${(item.iconLocation.light || item.iconLocation.dark).toString()}")`); + createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${item.iconLocation.dark.toString()}")`); + MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); } - this.$e.getHTMLElement().classList.add('icon', iconClass); - this._itemClassDispose = { dispose: () => this.$e.getHTMLElement().classList.remove('icon', iconClass) }; + addClasses(this.label, 'icon', iconClass); + this._itemClassDispose = toDisposable(() => removeClasses(this.label, 'icon', iconClass)); } } @@ -271,6 +261,6 @@ export class ContextAwareMenuItemActionItem extends MenuItemActionItem { event.stopPropagation(); this.actionRunner.run(this._commandAction, this._context) - .done(undefined, err => this._notificationService.error(err)); + .then(undefined, err => this._notificationService.error(err)); } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 914b7ff2f31..720ad5d4de5 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -13,20 +13,28 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; +import { URI, UriComponents } from 'vs/base/common/uri'; export interface ILocalizedString { value: string; original: string; } -export interface ICommandAction { +export interface IBaseCommandAction { id: string; title: string | ILocalizedString; category?: string | ILocalizedString; - iconPath?: { dark: string; light?: string; }; +} + +export interface ICommandAction extends IBaseCommandAction { + iconLocation?: { dark: URI; light?: URI; }; precondition?: ContextKeyExpr; } +export interface ISerializableCommandAction extends IBaseCommandAction { + iconLocation?: { dark: UriComponents; light?: UriComponents; }; +} + export interface IMenuItem { command: ICommandAction; alt?: ICommandAction; @@ -35,6 +43,22 @@ export interface IMenuItem { order?: number; } +export interface ISubmenuItem { + title: string | ILocalizedString; + submenu: MenuId; + when?: ContextKeyExpr; + group?: 'navigation' | string; + order?: number; +} + +export function isIMenuItem(item: IMenuItem | ISubmenuItem): item is IMenuItem { + return (item as IMenuItem).command !== undefined; +} + +export function isISubmenuItem(item: IMenuItem | ISubmenuItem): item is ISubmenuItem { + return (item as ISubmenuItem).submenu !== undefined; +} + export class MenuId { private static ID = 1; @@ -61,6 +85,21 @@ export class MenuId { static readonly ViewItemContext = new MenuId(); static readonly TouchBarContext = new MenuId(); static readonly SearchContext = new MenuId(); + static readonly MenubarFileMenu = new MenuId(); + static readonly MenubarEditMenu = new MenuId(); + static readonly MenubarRecentMenu = new MenuId(); + static readonly MenubarSelectionMenu = new MenuId(); + static readonly MenubarViewMenu = new MenuId(); + static readonly MenubarAppearanceMenu = new MenuId(); + static readonly MenubarLayoutMenu = new MenuId(); + static readonly MenubarGoMenu = new MenuId(); + static readonly MenubarSwitchEditorMenu = new MenuId(); + static readonly MenubarSwitchGroupMenu = new MenuId(); + static readonly MenubarDebugMenu = new MenuId(); + static readonly MenubarNewBreakpointMenu = new MenuId(); + static readonly MenubarPreferencesMenu = new MenuId(); + static readonly MenubarHelpMenu = new MenuId(); + static readonly MenubarTerminalMenu = new MenuId(); readonly id: string = String(MenuId.ID++); } @@ -72,7 +111,7 @@ export interface IMenuActionOptions { export interface IMenu extends IDisposable { onDidChange: Event; - getActions(options?: IMenuActionOptions): [string, MenuItemAction[]][]; + getActions(options?: IMenuActionOptions): [string, (MenuItemAction | SubmenuItemAction)[]][]; } export const IMenuService = createDecorator('menuService'); @@ -87,15 +126,15 @@ export interface IMenuService { export interface IMenuRegistry { addCommand(userCommand: ICommandAction): boolean; getCommand(id: string): ICommandAction; - appendMenuItem(menu: MenuId, item: IMenuItem): IDisposable; - getMenuItems(loc: MenuId): IMenuItem[]; + appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable; + getMenuItems(loc: MenuId): (IMenuItem | ISubmenuItem)[]; } export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { private _commands: { [id: string]: ICommandAction } = Object.create(null); - private _menuItems: { [loc: string]: IMenuItem[] } = Object.create(null); + private _menuItems: { [loc: string]: (IMenuItem | ISubmenuItem)[] } = Object.create(null); addCommand(command: ICommandAction): boolean { const old = this._commands[command.id]; @@ -107,7 +146,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { return this._commands[id]; } - appendMenuItem({ id }: MenuId, item: IMenuItem): IDisposable { + appendMenuItem({ id }: MenuId, item: IMenuItem | ISubmenuItem): IDisposable { let array = this._menuItems[id]; if (!array) { this._menuItems[id] = array = [item]; @@ -124,7 +163,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { }; } - getMenuItems({ id }: MenuId): IMenuItem[] { + getMenuItems({ id }: MenuId): (IMenuItem | ISubmenuItem)[] { const result = this._menuItems[id] || []; if (id === MenuId.CommandPalette.id) { @@ -135,9 +174,12 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { return result; } - private _appendImplicitItems(result: IMenuItem[]) { + private _appendImplicitItems(result: (IMenuItem | ISubmenuItem)[]) { const set = new Set(); - for (const { command, alt } of result) { + + const temp = result.filter(item => { return isIMenuItem(item); }) as IMenuItem[]; + + for (const { command, alt } of temp) { set.add(command.id); if (alt) { set.add(alt.id); @@ -166,6 +208,16 @@ export class ExecuteCommandAction extends Action { } } +export class SubmenuItemAction extends Action { + // private _options: IMenuActionOptions; + + readonly item: ISubmenuItem; + constructor(item: ISubmenuItem) { + typeof item.title === 'string' ? super('', item.title, 'submenu') : super('', item.title.value, 'submenu'); + this.item = item; + } +} + export class MenuItemAction extends ExecuteCommandAction { private _options: IMenuActionOptions; diff --git a/src/vs/platform/actions/common/menu.ts b/src/vs/platform/actions/common/menu.ts index e3ad0820736..37c14316640 100644 --- a/src/vs/platform/actions/common/menu.ts +++ b/src/vs/platform/actions/common/menu.ts @@ -9,10 +9,10 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MenuId, MenuRegistry, MenuItemAction, IMenu, IMenuItem, IMenuActionOptions } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, MenuItemAction, IMenu, IMenuItem, IMenuActionOptions, ISubmenuItem, SubmenuItemAction, isIMenuItem } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -type MenuItemGroup = [string, IMenuItem[]]; +type MenuItemGroup = [string, (IMenuItem | ISubmenuItem)[]]; export class Menu implements IMenu { @@ -66,15 +66,14 @@ export class Menu implements IMenu { return this._onDidChange.event; } - getActions(options: IMenuActionOptions): [string, MenuItemAction[]][] { - const result: [string, MenuItemAction[]][] = []; + getActions(options: IMenuActionOptions): [string, (MenuItemAction | SubmenuItemAction)[]][] { + const result: [string, (MenuItemAction | SubmenuItemAction)[]][] = []; for (let group of this._menuGroups) { const [id, items] = group; - const activeActions: MenuItemAction[] = []; + const activeActions: (MenuItemAction | SubmenuItemAction)[] = []; for (const item of items) { if (this._contextKeyService.contextMatchesRules(item.when)) { - const action = new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService); - action.order = item.order; //TODO@Ben order is menu item property, not an action property + const action = isIMenuItem(item) ? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService) : new SubmenuItemAction(item); activeActions.push(action); } } diff --git a/src/vs/platform/backup/common/backup.ts b/src/vs/platform/backup/common/backup.ts index 1d77135f933..a761c98e0ae 100644 --- a/src/vs/platform/backup/common/backup.ts +++ b/src/vs/platform/backup/common/backup.ts @@ -5,11 +5,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; export interface IBackupWorkspacesFormat { rootWorkspaces: IWorkspaceIdentifier[]; - folderWorkspaces: string[]; + folderURIWorkspaces: string[]; emptyWorkspaces: string[]; + + // deprecated + folderWorkspaces?: string[]; // use folderURIWorkspaces instead } export const IBackupMainService = createDecorator('backupMainService'); @@ -20,10 +24,14 @@ export interface IBackupMainService { isHotExitEnabled(): boolean; getWorkspaceBackups(): IWorkspaceIdentifier[]; - getFolderBackupPaths(): string[]; + getFolderBackupPaths(): URI[]; getEmptyWindowBackupPaths(): string[]; registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string; - registerFolderBackupSync(folderPath: string): string; + registerFolderBackupSync(folderUri: URI): string; registerEmptyWindowBackupSync(backupFolder?: string): string; + + unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void; + unregisterFolderBackupSync(folderUri: URI): void; + unregisterEmptyWindowBackupSync(backupFolder: string): void; } \ No newline at end of file diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 9201f86adb9..cca5c578754 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -3,18 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as arrays from 'vs/base/common/arrays'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import * as platform from 'vs/base/common/platform'; import * as extfs from 'vs/base/node/extfs'; -import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup'; +import * as arrays from 'vs/base/common/arrays'; +import { IBackupMainService, IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources'; +import { isEqual } from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; export class BackupMainService implements IBackupMainService { @@ -23,7 +28,9 @@ export class BackupMainService implements IBackupMainService { protected backupHome: string; protected workspacesJsonPath: string; - protected backups: IBackupWorkspacesFormat; + protected rootWorkspaces: IWorkspaceIdentifier[]; + protected folderWorkspaces: URI[]; + protected emptyWorkspaces: string[]; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -43,17 +50,16 @@ export class BackupMainService implements IBackupMainService { return []; } - return this.backups.rootWorkspaces.slice(0); // return a copy + return this.rootWorkspaces.slice(0); // return a copy } - public getFolderBackupPaths(): string[] { + public getFolderBackupPaths(): URI[] { if (this.isHotExitOnExitAndWindowClose()) { // Only non-folder windows are restored on main process launch when // hot exit is configured as onExitAndWindowClose. return []; } - - return this.backups.folderWorkspaces.slice(0); // return a copy + return this.folderWorkspaces.slice(0); // return a copy } public isHotExitEnabled(): boolean { @@ -71,13 +77,16 @@ export class BackupMainService implements IBackupMainService { } public getEmptyWindowBackupPaths(): string[] { - return this.backups.emptyWorkspaces.slice(0); // return a copy + return this.emptyWorkspaces.slice(0); // return a copy } public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string { - this.pushBackupPathsSync(workspace, this.backups.rootWorkspaces); + if (!this.rootWorkspaces.some(w => w.id === workspace.id)) { + this.rootWorkspaces.push(workspace); + this.saveSync(); + } - const backupPath = path.join(this.backupHome, workspace.id); + const backupPath = this.getBackupPath(workspace.id); if (migrateFrom) { this.moveBackupFolderSync(backupPath, migrateFrom); @@ -103,10 +112,28 @@ export class BackupMainService implements IBackupMainService { } } - public registerFolderBackupSync(folderPath: string): string { - this.pushBackupPathsSync(folderPath, this.backups.folderWorkspaces); + public unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { + let index = arrays.firstIndex(this.rootWorkspaces, w => w.id === workspace.id); + if (index !== -1) { + this.rootWorkspaces.splice(index, 1); + this.saveSync(); + } + } - return path.join(this.backupHome, this.getFolderHash(folderPath)); + public registerFolderBackupSync(folderUri: URI): string { + if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri))) { + this.folderWorkspaces.push(folderUri); + this.saveSync(); + } + return this.getBackupPath(this.getFolderHash(folderUri)); + } + + public unregisterFolderBackupSync(folderUri: URI): void { + let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri)); + if (index !== -1) { + this.folderWorkspaces.splice(index, 1); + this.saveSync(); + } } public registerEmptyWindowBackupSync(backupFolder?: string): string { @@ -115,52 +142,23 @@ export class BackupMainService implements IBackupMainService { if (!backupFolder) { backupFolder = this.getRandomEmptyWindowId(); } - - this.pushBackupPathsSync(backupFolder, this.backups.emptyWorkspaces); - - return path.join(this.backupHome, backupFolder); + if (!this.emptyWorkspaces.some(w => isEqual(w, backupFolder, !platform.isLinux))) { + this.emptyWorkspaces.push(backupFolder); + this.saveSync(); + } + return this.getBackupPath(backupFolder); } - private pushBackupPathsSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { - if (this.indexOf(workspaceIdentifier, target) === -1) { - target.push(workspaceIdentifier); + public unregisterEmptyWindowBackupSync(backupFolder: string): void { + let index = arrays.firstIndex(this.emptyWorkspaces, w => isEqual(w, backupFolder, !platform.isLinux)); + if (index !== -1) { + this.emptyWorkspaces.splice(index, 1); this.saveSync(); } } - protected removeBackupPathSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void { - if (!target) { - return; - } - - const index = this.indexOf(workspaceIdentifier, target); - if (index === -1) { - return; - } - - target.splice(index, 1); - this.saveSync(); - } - - private indexOf(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): number { - if (!target) { - return -1; - } - - const sanitizedWorkspaceIdentifier = this.sanitizeId(workspaceIdentifier); - - return arrays.firstIndex(target, id => this.sanitizeId(id) === sanitizedWorkspaceIdentifier); - } - - private sanitizeId(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string { - if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return this.sanitizePath(workspaceIdentifier); - } - - return workspaceIdentifier.id; - } - protected loadSync(): void { + let backups: IBackupWorkspacesFormat; try { backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here @@ -168,117 +166,168 @@ export class BackupMainService implements IBackupMainService { backups = Object.create(null); } - // Ensure rootWorkspaces is a object[] - if (backups.rootWorkspaces) { - const rws = backups.rootWorkspaces; - if (!Array.isArray(rws) || rws.some(r => typeof r !== 'object')) { - backups.rootWorkspaces = []; - } - } else { - backups.rootWorkspaces = []; - } + // read empty worrkspace backs first + this.emptyWorkspaces = this.validateEmptyWorkspaces(backups.emptyWorkspaces); - // Ensure folderWorkspaces is a string[] - if (backups.folderWorkspaces) { - const fws = backups.folderWorkspaces; - if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) { - backups.folderWorkspaces = []; - } - } else { - backups.folderWorkspaces = []; - } + // read workspace backups + this.rootWorkspaces = this.validateWorkspaces(backups.rootWorkspaces); - // Ensure emptyWorkspaces is a string[] - if (backups.emptyWorkspaces) { - const fws = backups.emptyWorkspaces; - if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) { - backups.emptyWorkspaces = []; - } - } else { - backups.emptyWorkspaces = []; - } - - this.backups = this.dedupeBackups(backups); - - // Validate backup workspaces - this.validateBackupWorkspaces(backups); - } - - protected dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - - // De-duplicate folder/workspace backups. don't worry about cleaning them up any duplicates as - // they will be removed when there are no backups. - backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws)); - backups.rootWorkspaces = arrays.distinct(backups.rootWorkspaces, ws => this.sanitizePath(ws.id)); - - return backups; - } - - private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void { - const staleBackupWorkspaces: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; backupPath: string; target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; - - const workspaceAndFolders: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = []; - workspaceAndFolders.push(...backups.rootWorkspaces.map(r => ({ workspaceIdentifier: r, target: backups.rootWorkspaces }))); - workspaceAndFolders.push(...backups.folderWorkspaces.map(f => ({ workspaceIdentifier: f, target: backups.folderWorkspaces }))); - - // Validate Workspace and Folder Backups - workspaceAndFolders.forEach(workspaceOrFolder => { - const workspaceId = workspaceOrFolder.workspaceIdentifier; - const workspacePath = isSingleFolderWorkspaceIdentifier(workspaceId) ? workspaceId : workspaceId.configPath; - const backupPath = path.join(this.backupHome, isSingleFolderWorkspaceIdentifier(workspaceId) ? this.getFolderHash(workspaceId) : workspaceId.id); - const hasBackups = this.hasBackupsSync(backupPath); - const missingWorkspace = hasBackups && !fs.existsSync(workspacePath); - - // If the workspace/folder has no backups, make sure to delete it - // If the workspace/folder has backups, but the target workspace is missing, convert backups to empty ones - if (!hasBackups || missingWorkspace) { - staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target }); - - if (missingWorkspace) { - this.convertToEmptyWindowBackup(backupPath); + // read folder backups + let workspaceFolders: URI[]; + try { + if (Array.isArray(backups.folderURIWorkspaces)) { + workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f)); + } else if (Array.isArray(backups.folderWorkspaces)) { + // migrate legacy folder paths + workspaceFolders = []; + for (const folderPath of backups.folderWorkspaces) { + const oldFolderHash = this.getLegacyFolderHash(folderPath); + const folderUri = URI.file(folderPath); + const newFolderHash = this.getFolderHash(folderUri); + if (newFolderHash !== oldFolderHash) { + this.moveBackupFolderSync(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash)); + } + workspaceFolders.push(folderUri); } } - }); + } catch (e) { + // ignore URI parsing exceptions + } + this.folderWorkspaces = this.validateFolders(workspaceFolders); + + // save again in case some workspaces or folders have been removed + this.saveSync(); + + } + + private getBackupPath(oldFolderHash: string): string { + return path.join(this.backupHome, oldFolderHash); + } + + private validateWorkspaces(rootWorkspaces: IWorkspaceIdentifier[]): IWorkspaceIdentifier[] { + if (!Array.isArray(rootWorkspaces)) { + return []; + } + + const seenIds: { [id: string]: boolean } = Object.create(null); + const result: IWorkspaceIdentifier[] = []; + + // Validate Workspaces + for (let workspace of rootWorkspaces) { + if (!isWorkspaceIdentifier(workspace)) { + return []; // wrong format, skip all entries + } + + if (!seenIds[workspace.id]) { + seenIds[workspace.id] = true; + + const backupPath = this.getBackupPath(workspace.id); + const hasBackups = this.hasBackupsSync(backupPath); + + // If the workspace has no backups, ignore it + if (hasBackups) { + if (fs.existsSync(workspace.configPath)) { + result.push(workspace); + } else { + // If the workspace has backups, but the target workspace is missing, convert backups to empty ones + this.convertToEmptyWindowBackup(backupPath); + } + } else { + this.deleteStaleBackup(backupPath); + } + } + } + return result; + } + + private validateFolders(folderWorkspaces: URI[]): URI[] { + if (!Array.isArray(folderWorkspaces)) { + return []; + } + + const result: URI[] = []; + const seen: { [id: string]: boolean } = Object.create(null); + + for (let folderURI of folderWorkspaces) { + const key = getComparisonKey(folderURI); + if (!seen[key]) { + seen[key] = true; + + const backupPath = this.getBackupPath(this.getFolderHash(folderURI)); + const hasBackups = this.hasBackupsSync(backupPath); + + // If the folder has no backups, ignore it + if (hasBackups) { + if (folderURI.scheme !== Schemas.file || fs.existsSync(folderURI.fsPath)) { + result.push(folderURI); + } else { + // If the folder has backups, but the target workspace is missing, convert backups to empty ones + this.convertToEmptyWindowBackup(backupPath); + } + } else { + this.deleteStaleBackup(backupPath); + } + } + } + + return result; + } + private validateEmptyWorkspaces(emptyWorkspaces: string[]): string[] { + if (!Array.isArray(emptyWorkspaces)) { + return []; + } + + const result: string[] = []; + const seen: { [id: string]: boolean } = Object.create(null); // Validate Empty Windows - backups.emptyWorkspaces.forEach(backupFolder => { - const backupPath = path.join(this.backupHome, backupFolder); - if (!this.hasBackupsSync(backupPath)) { - staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, target: backups.emptyWorkspaces }); + for (let backupFolder of emptyWorkspaces) { + if (typeof backupFolder !== 'string') { + return []; } - }); - // Clean up stale backups - staleBackupWorkspaces.forEach(staleBackupWorkspace => { - const { backupPath, workspaceIdentifier, target } = staleBackupWorkspace; + if (!seen[backupFolder]) { + seen[backupFolder] = true; - try { + const backupPath = this.getBackupPath(backupFolder); + if (this.hasBackupsSync(backupPath)) { + result.push(backupFolder); + } else { + this.deleteStaleBackup(backupPath); + } + } + } + + return result; + } + + private deleteStaleBackup(backupPath: string) { + try { + if (fs.existsSync(backupPath)) { extfs.delSync(backupPath); - } catch (ex) { - this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); } - - this.removeBackupPathSync(workspaceIdentifier, target); - }); + } catch (ex) { + this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); + } } private convertToEmptyWindowBackup(backupPath: string): boolean { // New empty window backup - const identifier = this.getRandomEmptyWindowId(); - this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces); + let newBackupFolder = this.getRandomEmptyWindowId(); + while (this.emptyWorkspaces.some(w => isEqual(w, newBackupFolder, platform.isLinux))) { + newBackupFolder = this.getRandomEmptyWindowId(); + } // Rename backupPath to new empty window backup path - const newEmptyWindowBackupPath = path.join(this.backupHome, identifier); + const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { fs.renameSync(backupPath, newEmptyWindowBackupPath); } catch (ex) { this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); - - this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces); - return false; } + this.emptyWorkspaces.push(newBackupFolder); return true; } @@ -308,8 +357,12 @@ export class BackupMainService implements IBackupMainService { if (!fs.existsSync(this.backupHome)) { fs.mkdirSync(this.backupHome); } - - extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.backups)); + const backups: IBackupWorkspacesFormat = { + rootWorkspaces: this.rootWorkspaces, + folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()), + emptyWorkspaces: this.emptyWorkspaces + }; + extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(backups)); } catch (ex) { this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); } @@ -319,11 +372,19 @@ export class BackupMainService implements IBackupMainService { return (Date.now() + Math.round(Math.random() * 1000)).toString(); } - private sanitizePath(p: string): string { - return platform.isLinux ? p : p.toLowerCase(); + protected getFolderHash(folderUri: URI): string { + let key; + if (folderUri.scheme === Schemas.file) { + // for backward compatibility, use the fspath as key + key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); + + } else { + key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString(); + } + return crypto.createHash('md5').update(key).digest('hex'); } - protected getFolderHash(folderPath: string): string { - return crypto.createHash('md5').update(this.sanitizePath(folderPath)).digest('hex'); + protected getLegacyFolderHash(folderPath: string): string { + return crypto.createHash('md5').update(platform.isLinux ? folderPath : folderPath.toLowerCase()).digest('hex'); } -} +} \ No newline at end of file diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index ff421ddbdf7..3ed740d5768 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; @@ -25,6 +25,11 @@ import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; suite('BackupMainService', () => { + + function assertEqualUris(actual: Uri[], expected: Uri[]) { + assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); + } + const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice'); const backupHome = path.join(parentDir, 'Backups'); const backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); @@ -43,28 +48,21 @@ suite('BackupMainService', () => { this.loadSync(); } - public get backupsData(): IBackupWorkspacesFormat { - return this.backups; - } - - public removeBackupPathSync(workspaceIdentifier: string | IWorkspaceIdentifier, target: (string | IWorkspaceIdentifier)[]): void { - return super.removeBackupPathSync(workspaceIdentifier, target); - } - public loadSync(): void { super.loadSync(); } - public dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat { - return super.dedupeBackups(backups); + public toBackupPath(arg: Uri | string): string { + const id = arg instanceof Uri ? super.getFolderHash(arg) : arg; + return path.join(this.backupHome, id); } - public toBackupPath(workspacePath: string): string { - return path.join(this.backupHome, super.getFolderHash(workspacePath)); + public getFolderHash(folderUri: Uri): string { + return super.getFolderHash(folderUri); } - public getFolderHash(folderPath: string): string { - return super.getFolderHash(folderPath); + public toLegacyBackupPath(folderPath: string): string { + return path.join(this.backupHome, super.getLegacyFolderHash(folderPath)); } } @@ -75,6 +73,31 @@ suite('BackupMainService', () => { }; } + async function ensureFolderExists(uri: Uri): Promise { + if (!fs.existsSync(uri.fsPath)) { + fs.mkdirSync(uri.fsPath); + } + const backupFolder = service.toBackupPath(uri); + await createBackupFolder(backupFolder); + } + + async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { + if (!fs.existsSync(workspace.configPath)) { + await pfs.writeFile(workspace.configPath, 'Hello'); + } + const backupFolder = service.toBackupPath(workspace.id); + await createBackupFolder(backupFolder); + return workspace; + } + + async function createBackupFolder(backupFolder: string): Promise { + if (!fs.existsSync(backupFolder)) { + fs.mkdirSync(backupFolder); + fs.mkdirSync(path.join(backupFolder, Schemas.file)); + await pfs.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello'); + } + } + function sanitizePath(p: string): string { return platform.isLinux ? p : p.toLowerCase(); } @@ -82,6 +105,8 @@ suite('BackupMainService', () => { const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar'); + const existingTestFolder1 = Uri.file(path.join(parentDir, 'folder1')); + let service: TestBackupMainService; let configService: TestConfigurationService; @@ -103,40 +128,40 @@ suite('BackupMainService', () => { this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); // 2) backup workspace path exists with empty contents within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assertEqualUris(service.getFolderBackupPaths(), []); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 3) backup workspace path exists with empty folders within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file)); - fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled)); - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file)); + fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled)); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assertEqualUris(service.getFolderBackupPaths(), []); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup - const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file); - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); - service.registerFolderBackupSync(fooFile.fsPath); + service.registerFolderBackupSync(fooFile); assert.equal(service.getFolderBackupPaths().length, 1); assert.equal(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); @@ -155,32 +180,32 @@ suite('BackupMainService', () => { assert.deepEqual(service.getWorkspaceBackups(), []); // 2) backup workspace path exists with empty contents within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); service.loadSync(); assert.deepEqual(service.getWorkspaceBackups(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 3) backup workspace path exists with empty folders within - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); - fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file)); - fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled)); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); + fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file)); + fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled)); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath)); service.loadSync(); assert.deepEqual(service.getWorkspaceBackups(), []); - assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath))); - assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath))); + assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); + assert.ok(!fs.existsSync(service.toBackupPath(barFile))); // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup - const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file); - fs.mkdirSync(service.toBackupPath(fooFile.fsPath)); - fs.mkdirSync(service.toBackupPath(barFile.fsPath)); + const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file); + fs.mkdirSync(service.toBackupPath(fooFile)); + fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath)); assert.equal(service.getWorkspaceBackups().length, 1); @@ -192,10 +217,10 @@ suite('BackupMainService', () => { }); test('service supports to migrate backup data from another location', () => { - const backupPathToMigrate = service.toBackupPath(fooFile.fsPath); + const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToMigrate); + service.registerFolderBackupSync(Uri.file(backupPathToMigrate)); const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate); @@ -208,15 +233,15 @@ suite('BackupMainService', () => { }); test('service backup migration makes sure to preserve existing backups', () => { - const backupPathToMigrate = service.toBackupPath(fooFile.fsPath); + const backupPathToMigrate = service.toBackupPath(fooFile); fs.mkdirSync(backupPathToMigrate); fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToMigrate); + service.registerFolderBackupSync(Uri.file(backupPathToMigrate)); - const backupPathToPreserve = service.toBackupPath(barFile.fsPath); + const backupPathToPreserve = service.toBackupPath(barFile); fs.mkdirSync(backupPathToPreserve); fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data'); - service.registerFolderBackupSync(backupPathToPreserve); + service.registerFolderBackupSync(Uri.file(backupPathToPreserve)); const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate); @@ -229,56 +254,102 @@ suite('BackupMainService', () => { assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0])).length); }); + suite('migrate folderPath to folderURI', () => { + + test('migration makes sure to preserve existing backups', async () => { + if (platform.isLinux) { + return; // TODO:Martin #54483 fix tests + } + + let path1 = path.join(parentDir, 'folder1').toLowerCase(); + let path2 = path.join(parentDir, 'folder2').toUpperCase(); + let uri1 = Uri.file(path1); + let uri2 = Uri.file(path2); + + if (!fs.existsSync(path1)) { + fs.mkdirSync(path1); + } + if (!fs.existsSync(path2)) { + fs.mkdirSync(path2); + } + const backupFolder1 = service.toLegacyBackupPath(path1); + if (!fs.existsSync(backupFolder1)) { + fs.mkdirSync(backupFolder1); + fs.mkdirSync(path.join(backupFolder1, Schemas.file)); + await pfs.writeFile(path.join(backupFolder1, Schemas.file, 'unsaved1.txt'), 'Legacy'); + } + const backupFolder2 = service.toLegacyBackupPath(path2); + if (!fs.existsSync(backupFolder2)) { + fs.mkdirSync(backupFolder2); + fs.mkdirSync(path.join(backupFolder2, Schemas.file)); + await pfs.writeFile(path.join(backupFolder2, Schemas.file, 'unsaved2.txt'), 'Legacy'); + } + + const workspacesJson = { rootWorkspaces: [], folderWorkspaces: [path1, path2], emptyWorkspaces: [] }; + await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { + const json = JSON.parse(content); + assert.deepEqual(json.folderURIWorkspaces, [uri1.toString(), uri2.toString()]); + const newBackupFolder1 = service.toBackupPath(uri1); + assert.ok(fs.existsSync(path.join(newBackupFolder1, Schemas.file, 'unsaved1.txt'))); + const newBackupFolder2 = service.toBackupPath(uri2); + assert.ok(fs.existsSync(path.join(newBackupFolder2, Schemas.file, 'unsaved2.txt'))); + }); + }); + }); + }); + suite('loadSync', () => { test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => { fs.writeFileSync(backupWorkspacesPath, ''); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => { fs.writeFileSync(backupWorkspacesPath, '{}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}'); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', () => { - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); + assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); service.loadSync(); - assert.deepEqual(service.getFolderBackupPaths(), []); + assertEqualUris(service.getFolderBackupPaths(), []); }); test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { @@ -379,59 +450,82 @@ suite('BackupMainService', () => { }); suite('dedupeFolderWorkspaces', () => { - test('should ignore duplicates on Windows and Mac (folder workspace)', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } + test('should ignore duplicates (folder workspace)', async () => { - const backups: IBackupWorkspacesFormat = { + await ensureFolderExists(existingTestFolder1); + + const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], - folderWorkspaces: platform.isWindows ? ['c:\\FOO', 'C:\\FOO', 'c:\\foo'] : ['/FOO', '/foo'], + folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()], emptyWorkspaces: [] }; - - service.dedupeBackups(backups); - - assert.equal(backups.folderWorkspaces.length, 1); - if (platform.isWindows) { - assert.deepEqual(backups.folderWorkspaces, ['c:\\FOO'], 'should return the first duplicated entry'); - } else { - assert.deepEqual(backups.folderWorkspaces, ['/FOO'], 'should return the first duplicated entry'); - } + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + }); + }); }); - test('should ignore duplicates on Windows and Mac (root workspace)', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } + test('should ignore duplicates on Windows and Mac (folder workspace)', async () => { - const backups: IBackupWorkspacesFormat = { - rootWorkspaces: platform.isWindows ? [toWorkspace('c:\\FOO'), toWorkspace('C:\\FOO'), toWorkspace('c:\\foo')] : [toWorkspace('/FOO'), toWorkspace('/foo')], - folderWorkspaces: [], + await ensureFolderExists(existingTestFolder1); + + const workspacesJson: IBackupWorkspacesFormat = { + rootWorkspaces: [], + folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()], emptyWorkspaces: [] }; + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + }); + }); + }); - service.dedupeBackups(backups); - - assert.equal(backups.rootWorkspaces.length, 1); - if (platform.isWindows) { - assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['c:\\FOO'], 'should return the first duplicated entry'); - } else { - assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['/FOO'], 'should return the first duplicated entry'); + test('should ignore duplicates on Windows and Mac (root workspace)', async () => { + if (platform.isLinux) { + return; // TODO:Martin #54483 fix tests } + + const workspacePath = path.join(parentDir, 'Foo.code-workspace'); + + const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath)); + const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath.toUpperCase())); + const workspace3 = await ensureWorkspaceExists(toWorkspace(workspacePath.toLowerCase())); + + const workspacesJson: IBackupWorkspacesFormat = { + rootWorkspaces: [workspace1, workspace2, workspace3], + folderURIWorkspaces: [], + emptyWorkspaces: [] + }; + return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { + service.loadSync(); + return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { + const json = JSON.parse(buffer); + assert.equal(json.rootWorkspaces.length, platform.isLinux ? 3 : 1); + if (platform.isLinux) { + assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath, workspacePath.toUpperCase(), workspacePath.toLowerCase()]); + } else { + assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath], 'should return the first duplicated entry'); + } + }); + }); + }); }); suite('registerWindowForBackups', () => { test('should persist paths to workspaces.json (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath, barFile.fsPath]); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); + assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath, barFile.fsPath]); + assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); }); @@ -455,11 +549,11 @@ suite('BackupMainService', () => { }); test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); - assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); + assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath.toUpperCase()]); + assert.deepEqual(json.folderURIWorkspaces, [Uri.file(fooFile.fsPath.toUpperCase()).toString()]); }); }); @@ -475,16 +569,16 @@ suite('BackupMainService', () => { suite('removeBackupPathSync', () => { test('should remove folder workspaces from workspaces.json (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(barFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(barFile); + service.unregisterFolderBackupSync(fooFile); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); - assert.deepEqual(json.folderWorkspaces, [barFile.fsPath]); - service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); + assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + service.unregisterFolderBackupSync(barFile); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); - assert.deepEqual(json2.folderWorkspaces, []); + assert.deepEqual(json2.folderURIWorkspaces, []); }); }); }); @@ -494,11 +588,11 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws1); const ws2 = toWorkspace(barFile.fsPath); service.registerWorkspaceBackupSync(ws2); - service.removeBackupPathSync(ws1, service.backupsData.rootWorkspaces); + service.unregisterWorkspaceBackupSync(ws1); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [barFile.fsPath]); - service.removeBackupPathSync(ws2, service.backupsData.rootWorkspaces); + service.unregisterWorkspaceBackupSync(ws2); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.rootWorkspaces, []); @@ -509,11 +603,11 @@ suite('BackupMainService', () => { test('should remove empty workspaces from workspaces.json', () => { service.registerEmptyWindowBackupSync('foo'); service.registerEmptyWindowBackupSync('bar'); - service.removeBackupPathSync('foo', service.backupsData.emptyWorkspaces); + service.unregisterEmptyWindowBackupSync('foo'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { const json = JSON.parse(buffer); assert.deepEqual(json.emptyWorkspaces, ['bar']); - service.removeBackupPathSync('bar', service.backupsData.emptyWorkspaces); + service.unregisterEmptyWindowBackupSync('bar'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json2 = JSON.parse(content); assert.deepEqual(json2.emptyWorkspaces, []); @@ -521,23 +615,24 @@ suite('BackupMainService', () => { }); }); - test('should fail gracefully when removing a path that doesn\'t exist', () => { - const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderWorkspaces: [fooFile.fsPath], emptyWorkspaces: [] }; + test('should fail gracefully when removing a path that doesn\'t exist', async () => { + + await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync + + const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaces: [] }; return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => { - service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces); - service.removeBackupPathSync('test', service.backupsData.emptyWorkspaces); + service.loadSync(); + service.unregisterFolderBackupSync(barFile); + service.unregisterEmptyWindowBackupSync('test'); return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { const json = JSON.parse(content); - assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath]); + assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); }); }); }); suite('getWorkspaceHash', () => { - test('should perform an md5 hash on the path', () => { - assert.equal(service.getFolderHash('/foo'), '1effb2475fcfba4f9e8b8a1dbc8f3caf'); - }); test('should ignore case on Windows and Mac', () => { // Skip test on Linux @@ -546,19 +641,19 @@ suite('BackupMainService', () => { } if (platform.isMacintosh) { - assert.equal(service.getFolderHash('/foo'), service.getFolderHash('/FOO')); + assert.equal(service.getFolderHash(Uri.file('/foo')), service.getFolderHash(Uri.file('/FOO'))); } if (platform.isWindows) { - assert.equal(service.getFolderHash('c:\\foo'), service.getFolderHash('C:\\FOO')); + assert.equal(service.getFolderHash(Uri.file('c:\\foo')), service.getFolderHash(Uri.file('C:\\FOO'))); } }); }); suite('mixed path casing', () => { test('should handle case insensitive paths properly (registerWindowForBackupsSync) (folder workspace)', () => { - service.registerFolderBackupSync(fooFile.fsPath); - service.registerFolderBackupSync(fooFile.fsPath.toUpperCase()); + service.registerFolderBackupSync(fooFile); + service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { assert.equal(service.getFolderBackupPaths().length, 2); @@ -581,13 +676,13 @@ suite('BackupMainService', () => { test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => { // same case - service.registerFolderBackupSync(fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.unregisterFolderBackupSync(fooFile); assert.equal(service.getFolderBackupPaths().length, 0); // mixed case - service.registerFolderBackupSync(fooFile.fsPath); - service.removeBackupPathSync(fooFile.fsPath.toUpperCase(), service.backupsData.folderWorkspaces); + service.registerFolderBackupSync(fooFile); + service.unregisterFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { assert.equal(service.getFolderBackupPaths().length, 1); diff --git a/src/vs/platform/clipboard/common/clipboardService.ts b/src/vs/platform/clipboard/common/clipboardService.ts index d93af3a745f..13b9c84de3c 100644 --- a/src/vs/platform/clipboard/common/clipboardService.ts +++ b/src/vs/platform/clipboard/common/clipboardService.ts @@ -6,7 +6,7 @@ 'use strict'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export const IClipboardService = createDecorator('clipboardService'); diff --git a/src/vs/platform/clipboard/electron-browser/clipboardService.ts b/src/vs/platform/clipboard/electron-browser/clipboardService.ts index d59c3edaf4d..b2861173923 100644 --- a/src/vs/platform/clipboard/electron-browser/clipboardService.ts +++ b/src/vs/platform/clipboard/electron-browser/clipboardService.ts @@ -7,7 +7,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { clipboard } from 'electron'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; export class ClipboardService implements IClipboardService { diff --git a/src/vs/platform/commands/common/commands.ts b/src/vs/platform/commands/common/commands.ts index 7d52c3ecaa0..b912a022dfc 100644 --- a/src/vs/platform/commands/common/commands.ts +++ b/src/vs/platform/commands/common/commands.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TypeConstraint, validateConstraints } from 'vs/base/common/types'; import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; @@ -46,6 +46,7 @@ export interface ICommandHandlerDescription { export interface ICommandRegistry { registerCommand(id: string, command: ICommandHandler): IDisposable; registerCommand(command: ICommand): IDisposable; + registerCommandAlias(oldId: string, newId: string): IDisposable; getCommand(id: string): ICommand; getCommands(): ICommandsMap; } @@ -91,14 +92,18 @@ export const CommandsRegistry: ICommandRegistry = new class implements ICommandR let removeFn = commands.unshift(idOrCommand); - return { - dispose: () => { - removeFn(); - if (this._commands.get(id).isEmpty()) { - this._commands.delete(id); - } + return toDisposable(() => { + removeFn(); + if (this._commands.get(id).isEmpty()) { + this._commands.delete(id); } - }; + }); + } + + registerCommandAlias(oldId: string, newId: string): IDisposable { + return CommandsRegistry.registerCommand(oldId, (accessor, ...args) => { + accessor.get(ICommandService).executeCommand(newId, ...args); + }); } getCommand(id: string): ICommand { diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 6aa47675ddb..13f6cdf5146 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as objects from 'vs/base/common/objects'; import * as types from 'vs/base/common/types'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -28,13 +28,22 @@ export interface IConfigurationOverrides { resource?: URI; } -export enum ConfigurationTarget { +export const enum ConfigurationTarget { USER = 1, WORKSPACE, WORKSPACE_FOLDER, DEFAULT, MEMORY } +export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) { + switch (configurationTarget) { + case ConfigurationTarget.USER: return 'USER'; + case ConfigurationTarget.WORKSPACE: return 'WORKSPACE'; + case ConfigurationTarget.WORKSPACE_FOLDER: return 'WORKSPACE_FOLDER'; + case ConfigurationTarget.DEFAULT: return 'DEFAULT'; + case ConfigurationTarget.MEMORY: return 'MEMORY'; + } +} export interface IConfigurationChangeEvent { diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 9944e02325f..acf80b147b8 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -9,7 +9,7 @@ import { ResourceMap } from 'vs/base/common/map'; import * as arrays from 'vs/base/common/arrays'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IOverrides, overrideIdentifierFromKey, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, IConfigurationChangeEvent, ConfigurationTarget, removeFromValueTree, toOverrides } from 'vs/platform/configuration/common/configuration'; import { Workspace } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 30d8205f48d..a9e2e987d88 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -34,6 +34,12 @@ export interface IConfigurationRegistry { */ notifyConfigurationSchemaUpdated(configuration: IConfigurationNode): void; + /** + * Event that fires whenver a configuration has been + * registered. + */ + onDidSchemaChange: Event; + /** * Event that fires whenver a configuration has been * registered. @@ -61,7 +67,7 @@ export interface IConfigurationRegistry { registerOverrideIdentifiers(identifiers: string[]): void; } -export enum ConfigurationScope { +export const enum ConfigurationScope { APPLICATION = 1, WINDOW, RESOURCE, @@ -70,8 +76,8 @@ export enum ConfigurationScope { export interface IConfigurationPropertySchema extends IJSONSchema { overridable?: boolean; scope?: ConfigurationScope; - notMultiRootAdopted?: boolean; included?: boolean; + tags?: string[]; } export interface IConfigurationNode { @@ -84,6 +90,7 @@ export interface IConfigurationNode { allOf?: IConfigurationNode[]; overridable?: boolean; scope?: ConfigurationScope; + contributedByExtension?: boolean; } export interface IDefaultConfigurationExtension { @@ -109,6 +116,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { private overrideIdentifiers: string[] = []; private overridePropertyPattern: string; + private readonly _onDidSchemaChange: Emitter = new Emitter(); + readonly onDidSchemaChange: Event = this._onDidSchemaChange.event; + private readonly _onDidRegisterConfiguration: Emitter = new Emitter(); readonly onDidRegisterConfiguration: Event = this._onDidRegisterConfiguration.event; @@ -175,7 +185,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, scope: ConfigurationScope = ConfigurationScope.WINDOW, overridable: boolean = false): string[] { - scope = configuration.scope !== void 0 && configuration.scope !== null ? configuration.scope : scope; + scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope; overridable = configuration.overridable || overridable; let propertyKeys = []; let properties = configuration.properties; @@ -197,8 +207,11 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (overridable) { property.overridable = true; } - if (property.scope === void 0) { - property.scope = scope; + + if (OVERRIDE_PROPERTY_PATTERN.test(key)) { + property.scope = void 0; // No scope for overridable properties `[${identifier}]` + } else { + property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope; } // Add to properties maps @@ -239,7 +252,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { function register(configuration: IConfigurationNode) { let properties = configuration.properties; if (properties) { - for (let key in properties) { + for (const key in properties) { allSettings.properties[key] = properties[key]; switch (properties[key].scope) { case ConfigurationScope.APPLICATION: @@ -260,6 +273,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } } register(configuration); + this._onDidSchemaChange.fire(); } private updateSchemaForOverrideSettingsConfiguration(configuration: IConfigurationNode): void { @@ -291,6 +305,8 @@ class ConfigurationRegistry implements IConfigurationRegistry { applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties; windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties; resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties; + + this._onDidSchemaChange.fire(); } private update(configuration: IConfigurationNode): void { diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 20c2f5c5135..b7fa775beaf 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; suite('ConfigurationModel', () => { diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index 90c2324afe9..8c2a0fed9da 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -6,7 +6,7 @@ 'use strict'; import { TernarySearchTree } from 'vs/base/common/map'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { getConfigurationKeys, IConfigurationOverrides, IConfigurationService, getConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index b661d0e0c0e..2ac546ff0c0 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -331,6 +331,7 @@ class ScopedContextKeyService extends AbstractContextKeyService { this._parent.disposeContext(this._myContextId); if (this._domNode) { this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR); + this._domNode = undefined; } } diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 33888854f2c..5bc252f5502 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -8,7 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; -export enum ContextKeyExprType { +export const enum ContextKeyExprType { Defined = 1, Not = 2, Equals = 3, diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index c0dd673a14d..0ae68867b5c 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -6,16 +6,16 @@ 'use strict'; import 'vs/css!./contextMenuHandler'; -import { $, Builder } from 'vs/base/browser/builder'; -import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IActionRunner, ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions'; +import { ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions'; import { Menu } from 'vs/base/browser/ui/menu/menu'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; +import { addDisposableListener } from 'vs/base/browser/dom'; export class ContextMenuHandler { @@ -23,10 +23,10 @@ export class ContextMenuHandler { private notificationService: INotificationService; private telemetryService: ITelemetryService; - private actionRunner: IActionRunner; - private $el: Builder; + private element: HTMLElement; + private elementDisposable: IDisposable; private menuContainerElement: HTMLElement; - private toDispose: IDisposable[]; + private focusToReturn: HTMLElement; constructor(element: HTMLElement, contextViewService: IContextViewService, telemetryService: ITelemetryService, notificationService: INotificationService) { this.setContainer(element); @@ -35,56 +35,28 @@ export class ContextMenuHandler { this.telemetryService = telemetryService; this.notificationService = notificationService; - this.actionRunner = new ActionRunner(); this.menuContainerElement = null; - this.toDispose = []; - - let hideViewOnRun = false; - - this.toDispose.push(this.actionRunner.onDidBeforeRun((e: IRunEvent) => { - if (this.telemetryService) { - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' }); - } - - hideViewOnRun = !!(e).retainActionItem; - - if (!hideViewOnRun) { - this.contextViewService.hideContextView(false); - } - })); - - this.toDispose.push(this.actionRunner.onDidRun((e: IRunEvent) => { - if (hideViewOnRun) { - this.contextViewService.hideContextView(false); - } - - hideViewOnRun = false; - - if (e.error && this.notificationService) { - this.notificationService.error(e.error); - } - })); } public setContainer(container: HTMLElement): void { - if (this.$el) { - this.$el.off(['click', 'mousedown']); - this.$el = null; + if (this.element) { + this.elementDisposable = dispose(this.elementDisposable); + this.element = null; } if (container) { - this.$el = $(container); - this.$el.on('mousedown', (e: Event) => this.onMouseDown(e as MouseEvent)); + this.element = container; + this.elementDisposable = addDisposableListener(this.element, 'mousedown', (e) => this.onMouseDown(e as MouseEvent)); } } public showContextMenu(delegate: IContextMenuDelegate): void { - delegate.getActions().done((actions: IAction[]) => { + delegate.getActions().then((actions: IAction[]) => { + if (!actions.length) { + return; // Don't render an empty context menu + } + + this.focusToReturn = document.activeElement as HTMLElement; + this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, @@ -98,23 +70,25 @@ export class ContextMenuHandler { container.className += ' ' + className; } - let menu = new Menu(container, actions, { + const menuDisposables: IDisposable[] = []; + + const actionRunner = delegate.actionRunner || new ActionRunner(); + actionRunner.onDidBeforeRun(this.onActionRun, this, menuDisposables); + actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables); + + const menu = new Menu(container, actions, { actionItemProvider: delegate.getActionItem, context: delegate.getActionsContext ? delegate.getActionsContext() : null, - actionRunner: this.actionRunner + actionRunner, + getKeyBinding: delegate.getKeyBinding }); - let listener1 = menu.onDidCancel(() => { - this.contextViewService.hideContextView(true); - }); + menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables); + menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables); - let listener2 = menu.onDidBlur(() => { - this.contextViewService.hideContextView(true); - }); + menu.focus(!!delegate.autoSelectFirstItem); - menu.focus(); - - return combinedDisposable([listener1, listener2, menu]); + return combinedDisposable([...menuDisposables, menu]); }, onHide: (didCancel?: boolean) => { @@ -128,6 +102,31 @@ export class ContextMenuHandler { }); } + private onActionRun(e: IRunEvent): void { + if (this.telemetryService) { + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' }); + } + + this.contextViewService.hideContextView(false); + + // Restore focus here + if (this.focusToReturn) { + this.focusToReturn.focus(); + } + } + + private onDidActionRun(e: IRunEvent): void { + if (e.error && this.notificationService) { + this.notificationService.error(e.error); + } + } + private onMouseDown(e: MouseEvent): void { if (!this.menuContainerElement) { return; diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts new file mode 100644 index 00000000000..c02bebc7e24 --- /dev/null +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts @@ -0,0 +1,322 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceStats, collectWorkspaceStats, collectLaunchConfigs, WorkspaceStatItem } from 'vs/base/node/stats'; +import { IMainProcessInfo } from 'vs/platform/launch/electron-main/launchService'; +import { ProcessItem, listProcesses } from 'vs/base/node/ps'; +import product from 'vs/platform/node/product'; +import pkg from 'vs/platform/node/package'; +import * as os from 'os'; +import { virtualMachineHint } from 'vs/base/node/id'; +import { repeat, pad } from 'vs/base/common/strings'; +import { isWindows } from 'vs/base/common/platform'; +import { app } from 'electron'; +import { basename } from 'path'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ID = 'diagnosticsService'; +export const IDiagnosticsService = createDecorator(ID); + +export interface IDiagnosticsService { + _serviceBrand: any; + + formatEnvironment(info: IMainProcessInfo): string; + getPerformanceInfo(info: IMainProcessInfo): Promise; + getSystemInfo(info: IMainProcessInfo): SystemInfo; + printDiagnostics(info: IMainProcessInfo): Promise; +} + +export interface VersionInfo { + vscodeVersion: string; + os: string; +} + +export interface SystemInfo { + CPUs?: string; + 'Memory (System)': string; + 'Load (avg)'?: string; + VM: string; + 'Screen Reader': string; + 'Process Argv': string; + 'GPU Status': Electron.GPUFeatureStatus; +} + +export interface ProcessInfo { + cpu: number; + memory: number; + pid: number; + name: string; +} + +export interface PerformanceInfo { + processInfo?: string; + workspaceInfo?: string; +} + +export class DiagnosticsService implements IDiagnosticsService { + + _serviceBrand: any; + + formatEnvironment(info: IMainProcessInfo): string { + const MB = 1024 * 1024; + const GB = 1024 * MB; + + const output: string[] = []; + output.push(`Version: ${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`); + output.push(`OS Version: ${os.type()} ${os.arch()} ${os.release()}`); + const cpus = os.cpus(); + if (cpus && cpus.length > 0) { + output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`); + } + output.push(`Memory (System): ${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`); + if (!isWindows) { + output.push(`Load (avg): ${os.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS + } + output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`); + output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`); + output.push(`Process Argv: ${info.mainArguments.join(' ')}`); + output.push(`GPU Status: ${this.expandGPUFeatures()}`); + + return output.join('\n'); + } + + getPerformanceInfo(info: IMainProcessInfo): Promise { + return listProcesses(info.mainPID).then(rootProcess => { + const workspaceInfoMessages = []; + + // Workspace Stats + const workspaceStatPromises = []; + if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { + info.windows.forEach(window => { + if (window.folderURIs.length === 0) { + return; + } + + workspaceInfoMessages.push(`| Window (${window.title})`); + + window.folderURIs.forEach(uriComponents => { + const folderUri = URI.revive(uriComponents); + if (folderUri.scheme === 'file') { + const folder = folderUri.fsPath; + workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { + + let countMessage = `${stats.fileCount} files`; + if (stats.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`); + workspaceInfoMessages.push(this.formatWorkspaceStats(stats)); + + const launchConfigs = await collectLaunchConfigs(folder); + if (launchConfigs.length > 0) { + workspaceInfoMessages.push(this.formatLaunchConfigs(launchConfigs)); + } + })); + } else { + workspaceInfoMessages.push(`| Folder (${folderUri.toString()}): RPerformance stats not available.`); + } + }); + }); + } + + return Promise.all(workspaceStatPromises).then(() => { + return { + processInfo: this.formatProcessList(info, rootProcess), + workspaceInfo: workspaceInfoMessages.join('\n') + }; + }).catch(error => { + return { + processInfo: this.formatProcessList(info, rootProcess), + workspaceInfo: `Unable to calculate workspace stats: ${error}` + }; + }); + }); + } + + getSystemInfo(info: IMainProcessInfo): SystemInfo { + const MB = 1024 * 1024; + const GB = 1024 * MB; + + const systemInfo: SystemInfo = { + 'Memory (System)': `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`, + VM: `${Math.round((virtualMachineHint.value() * 100))}%`, + 'Screen Reader': `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`, + 'Process Argv': `${info.mainArguments.join(' ')}`, + 'GPU Status': app.getGPUFeatureStatus() + }; + + const cpus = os.cpus(); + if (cpus && cpus.length > 0) { + systemInfo.CPUs = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`; + } + + if (!isWindows) { + systemInfo['Load (avg)'] = `${os.loadavg().map(l => Math.round(l)).join(', ')}`; + } + + + return systemInfo; + } + + printDiagnostics(info: IMainProcessInfo): Promise { + return listProcesses(info.mainPID).then(rootProcess => { + + // Environment Info + console.log(''); + console.log(this.formatEnvironment(info)); + + // Process List + console.log(''); + console.log(this.formatProcessList(info, rootProcess)); + + // Workspace Stats + const workspaceStatPromises = []; + if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { + console.log(''); + console.log('Workspace Stats: '); + info.windows.forEach(window => { + if (window.folderURIs.length === 0) { + return; + } + + console.log(`| Window (${window.title})`); + + window.folderURIs.forEach(uriComponents => { + const folderUri = URI.revive(uriComponents); + if (folderUri.scheme === 'file') { + const folder = folderUri.fsPath; + workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { + let countMessage = `${stats.fileCount} files`; + if (stats.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + console.log(`| Folder (${basename(folder)}): ${countMessage}`); + console.log(this.formatWorkspaceStats(stats)); + + await collectLaunchConfigs(folder).then(launchConfigs => { + if (launchConfigs.length > 0) { + console.log(this.formatLaunchConfigs(launchConfigs)); + } + }); + }).catch(error => { + console.log(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); + })); + } else { + console.log(`| Folder (${folderUri.toString()}): Workspace stats not available.`); + } + }); + }); + } + + return Promise.all(workspaceStatPromises).then(() => { + console.log(''); + console.log(''); + }); + }); + } + + private formatWorkspaceStats(workspaceStats: WorkspaceStats): string { + const output: string[] = []; + const lineLength = 60; + let col = 0; + + const appendAndWrap = (name: string, count: number) => { + const item = ` ${name}(${count})`; + + if (col + item.length > lineLength) { + output.push(line); + line = '| '; + col = line.length; + } + else { + col += item.length; + } + line += item; + }; + + // File Types + let line = '| File types:'; + const maxShown = 10; + let max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length; + for (let i = 0; i < max; i++) { + const item = workspaceStats.fileTypes[i]; + appendAndWrap(item.name, item.count); + } + output.push(line); + + // Conf Files + if (workspaceStats.configFiles.length >= 0) { + line = '| Conf files:'; + col = 0; + workspaceStats.configFiles.forEach((item) => { + appendAndWrap(item.name, item.count); + }); + output.push(line); + } + + return output.join('\n'); + } + + private formatLaunchConfigs(configs: WorkspaceStatItem[]): string { + const output: string[] = []; + let line = '| Launch Configs:'; + configs.forEach(each => { + const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`; + line += item; + }); + output.push(line); + return output.join('\n'); + } + + private expandGPUFeatures(): string { + const gpuFeatures = app.getGPUFeatureStatus(); + const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length)); + // Make columns aligned by adding spaces after feature name + return Object.keys(gpuFeatures).map(feature => `${feature}: ${repeat(' ', longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n '); + } + + private formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string { + const mapPidToWindowTitle = new Map(); + info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); + + const output: string[] = []; + + output.push('CPU %\tMem MB\t PID\tProcess'); + + if (rootProcess) { + this.formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0); + } + + return output.join('\n'); + } + + private formatProcessItem(mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { + const isRoot = (indent === 0); + + const MB = 1024 * 1024; + + // Format name with indent + let name: string; + if (isRoot) { + name = `${product.applicationName} main`; + } else { + name = `${repeat(' ', indent)} ${item.name}`; + + if (item.name === 'window') { + name = `${name} (${mapPidToWindowTitle.get(item.pid)})`; + } + } + const memory = process.platform === 'win32' ? item.mem : (os.totalmem() * (item.mem / 100)); + output.push(`${pad(Number(item.load.toFixed(0)), 5, ' ')}\t${pad(Number((memory / MB).toFixed(0)), 6, ' ')}\t${pad(Number((item.pid).toFixed(0)), 6, ' ')}\t${name}`); + + // Recurse into children if any + if (Array.isArray(item.children)) { + item.children.forEach(child => this.formatProcessItem(mapPidToWindowTitle, output, child, indent + 1)); + } + } +} diff --git a/src/vs/platform/dialogs/common/dialogIpc.ts b/src/vs/platform/dialogs/common/dialogIpc.ts deleted file mode 100644 index 7d260b4e8f8..00000000000 --- a/src/vs/platform/dialogs/common/dialogIpc.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; -import Severity from 'vs/base/common/severity'; - -export interface IDialogChannel extends IChannel { - call(command: 'show'): TPromise; - call(command: 'confirm'): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class DialogChannel implements IDialogChannel { - - constructor(@IDialogService private dialogService: IDialogService) { - } - - call(command: string, args?: any[]): TPromise { - switch (command) { - case 'show': return this.dialogService.show(args[0], args[1], args[2]); - case 'confirm': return this.dialogService.confirm(args[0]); - } - return TPromise.wrapError(new Error('invalid command')); - } -} - -export class DialogChannelClient implements IDialogService { - - _serviceBrand: any; - - constructor(private channel: IDialogChannel) { } - - show(severity: Severity, message: string, options: string[]): TPromise { - return this.channel.call('show', [severity, message, options]); - } - - confirm(confirmation: IConfirmation): TPromise { - return this.channel.call('confirm', [confirmation]); - } -} \ No newline at end of file diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index f5b0f0f5462..a1b43d7785f 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import Severity from 'vs/base/common/severity'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { basename } from 'vs/base/common/paths'; import { localize } from 'vs/nls'; diff --git a/src/vs/platform/dialogs/node/dialogIpc.ts b/src/vs/platform/dialogs/node/dialogIpc.ts new file mode 100644 index 00000000000..c392844d3f4 --- /dev/null +++ b/src/vs/platform/dialogs/node/dialogIpc.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. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { Event } from 'vs/base/common/event'; + +export interface IDialogChannel extends IChannel { + call(command: 'show'): Thenable; + call(command: 'confirm'): Thenable; + call(command: string, arg?: any): Thenable; +} + +export class DialogChannel implements IDialogChannel { + + constructor(@IDialogService private dialogService: IDialogService) { } + + listen(event: string): Event { + throw new Error('No event found'); + } + + call(command: string, args?: any[]): Thenable { + switch (command) { + case 'show': return this.dialogService.show(args[0], args[1], args[2]); + case 'confirm': return this.dialogService.confirm(args[0]); + } + return TPromise.wrapError(new Error('invalid command')); + } +} + +export class DialogChannelClient implements IDialogService { + + _serviceBrand: any; + + constructor(private channel: IDialogChannel) { } + + show(severity: Severity, message: string, options: string[]): TPromise { + return TPromise.wrap(this.channel.call('show', [severity, message, options])); + } + + confirm(confirmation: IConfirmation): TPromise { + return TPromise.wrap(this.channel.call('confirm', [confirmation])); + } +} \ No newline at end of file diff --git a/src/vs/platform/dialogs/node/dialogService.ts b/src/vs/platform/dialogs/node/dialogService.ts index b1896b82a8f..4304af55184 100644 --- a/src/vs/platform/dialogs/node/dialogService.ts +++ b/src/vs/platform/dialogs/node/dialogService.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; +import { canceled } from 'vs/base/common/errors'; export class CommandLineDialogService implements IDialogService { @@ -31,7 +32,7 @@ export class CommandLineDialogService implements IDialogService { }); rl.once('SIGINT', () => { rl.close(); - promise.cancel(); + e(canceled()); }); }); return promise; @@ -64,4 +65,4 @@ export class CommandLineDialogService implements IDialogService { } as IConfirmationResult; }); } -} \ No newline at end of file +} diff --git a/src/vs/platform/download/common/download.ts b/src/vs/platform/download/common/download.ts new file mode 100644 index 00000000000..1ff68352bb5 --- /dev/null +++ b/src/vs/platform/download/common/download.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; + +export const IDownloadService = createDecorator('downloadService'); + +export interface IDownloadService { + + _serviceBrand: any; + + download(location: URI, file: string): TPromise; + +} \ No newline at end of file diff --git a/src/vs/platform/download/node/downloadIpc.ts b/src/vs/platform/download/node/downloadIpc.ts new file mode 100644 index 00000000000..675a725b3b1 --- /dev/null +++ b/src/vs/platform/download/node/downloadIpc.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { URI } from 'vs/base/common/uri'; +import * as path from 'path'; +import * as fs from 'fs'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { Event, Emitter, buffer } from 'vs/base/common/event'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { mkdirp } from 'vs/base/node/pfs'; +import { IURITransformer } from 'vs/base/common/uriIpc'; + +export type UploadResponse = Buffer | string | undefined; + +export function upload(uri: URI): Event { + const stream = new Emitter(); + const readstream = fs.createReadStream(uri.fsPath); + readstream.on('data', data => stream.fire(data)); + readstream.on('error', error => stream.fire(error.toString())); + readstream.on('close', () => stream.fire()); + return stream.event; +} + +export interface IDownloadServiceChannel extends IChannel { + listen(event: 'upload', uri: URI): Event; + listen(event: string, arg?: any): Event; +} + +export class DownloadServiceChannel implements IDownloadServiceChannel { + + constructor() { } + + listen(event: string, arg?: any): Event { + switch (event) { + case 'upload': return buffer(upload(URI.revive(arg))); + } + return undefined; + } + + call(command: string, arg?: any): TPromise { + throw new Error('No calls'); + } +} + +export class DownloadServiceChannelClient implements IDownloadService { + + _serviceBrand: any; + + constructor(private channel: IDownloadServiceChannel, private uriTransformer: IURITransformer) { } + + download(from: URI, to: string): TPromise { + from = this.uriTransformer.transformOutgoing(from); + const dirName = path.dirname(to); + let out: fs.WriteStream; + return new TPromise((c, e) => { + return mkdirp(dirName) + .then(() => { + out = fs.createWriteStream(to); + out.once('close', () => c(null)); + out.once('error', e); + const uploadStream = this.channel.listen('upload', from); + const disposable = uploadStream((result: UploadResponse) => { + if (result === void 0) { + out.end(); + disposable.dispose(); + c(null); + } else if (Buffer.isBuffer(result)) { + out.write(result); + } else if (typeof result === 'string') { + out.close(); + disposable.dispose(); + e(result); + } + }); + }); + }); + } +} \ No newline at end of file diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts deleted file mode 100644 index a069d1c087d..00000000000 --- a/src/vs/platform/driver/common/driver.ts +++ /dev/null @@ -1,279 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; - -export const ID = 'driverService'; -export const IDriver = createDecorator(ID); - -// !! Do not remove the following START and END markers, they are parsed by the smoketest build - -//*START -export interface IElement { - tagName: string; - className: string; - textContent: string; - attributes: { [name: string]: string; }; - children: IElement[]; - top: number; - left: number; -} - -export interface IDriver { - _serviceBrand: any; - - getWindowIds(): TPromise; - capturePage(windowId: number): TPromise; - reloadWindow(windowId: number): TPromise; - dispatchKeybinding(windowId: number, keybinding: string): TPromise; - click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; - doubleClick(windowId: number, selector: string): TPromise; - setValue(windowId: number, selector: string, text: string): TPromise; - getTitle(windowId: number): TPromise; - isActiveElement(windowId: number, selector: string): TPromise; - getElements(windowId: number, selector: string, recursive?: boolean): TPromise; - typeInEditor(windowId: number, selector: string, text: string): TPromise; - getTerminalBuffer(windowId: number, selector: string): TPromise; - writeInTerminal(windowId: number, selector: string, text: string): TPromise; -} -//*END - -export interface IDriverChannel extends IChannel { - call(command: 'getWindowIds'): TPromise; - call(command: 'capturePage'): TPromise; - call(command: 'reloadWindow', arg: number): TPromise; - call(command: 'dispatchKeybinding', arg: [number, string]): TPromise; - call(command: 'click', arg: [number, string, number | undefined, number | undefined]): TPromise; - call(command: 'doubleClick', arg: [number, string]): TPromise; - call(command: 'setValue', arg: [number, string, string]): TPromise; - call(command: 'getTitle', arg: [number]): TPromise; - call(command: 'isActiveElement', arg: [number, string]): TPromise; - call(command: 'getElements', arg: [number, string, boolean]): TPromise; - call(command: 'typeInEditor', arg: [number, string, string]): TPromise; - call(command: 'getTerminalBuffer', arg: [number, string]): TPromise; - call(command: 'writeInTerminal', arg: [number, string, string]): TPromise; - call(command: string, arg: any): TPromise; -} - -export class DriverChannel implements IDriverChannel { - - constructor(private driver: IDriver) { } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'getWindowIds': return this.driver.getWindowIds(); - case 'capturePage': return this.driver.capturePage(arg); - case 'reloadWindow': return this.driver.reloadWindow(arg); - case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); - case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); - case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]); - case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); - case 'getTitle': return this.driver.getTitle(arg[0]); - case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); - case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); - } - - return undefined; - } -} - -export class DriverChannelClient implements IDriver { - - _serviceBrand: any; - - constructor(private channel: IDriverChannel) { } - - getWindowIds(): TPromise { - return this.channel.call('getWindowIds'); - } - - capturePage(windowId: number): TPromise { - return this.channel.call('capturePage', windowId); - } - - reloadWindow(windowId: number): TPromise { - return this.channel.call('reloadWindow', windowId); - } - - dispatchKeybinding(windowId: number, keybinding: string): TPromise { - return this.channel.call('dispatchKeybinding', [windowId, keybinding]); - } - - click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): TPromise { - return this.channel.call('click', [windowId, selector, xoffset, yoffset]); - } - - doubleClick(windowId: number, selector: string): TPromise { - return this.channel.call('doubleClick', [windowId, selector]); - } - - setValue(windowId: number, selector: string, text: string): TPromise { - return this.channel.call('setValue', [windowId, selector, text]); - } - - getTitle(windowId: number): TPromise { - return this.channel.call('getTitle', [windowId]); - } - - isActiveElement(windowId: number, selector: string): TPromise { - return this.channel.call('isActiveElement', [windowId, selector]); - } - - getElements(windowId: number, selector: string, recursive: boolean): TPromise { - return this.channel.call('getElements', [windowId, selector, recursive]); - } - - typeInEditor(windowId: number, selector: string, text: string): TPromise { - return this.channel.call('typeInEditor', [windowId, selector, text]); - } - - getTerminalBuffer(windowId: number, selector: string): TPromise { - return this.channel.call('getTerminalBuffer', [windowId, selector]); - } - - writeInTerminal(windowId: number, selector: string, text: string): TPromise { - return this.channel.call('writeInTerminal', [windowId, selector, text]); - } -} - -export interface IDriverOptions { - verbose: boolean; -} - -export interface IWindowDriverRegistry { - registerWindowDriver(windowId: number): TPromise; - reloadWindowDriver(windowId: number): TPromise; -} - -export interface IWindowDriverRegistryChannel extends IChannel { - call(command: 'registerWindowDriver', arg: number): TPromise; - call(command: 'reloadWindowDriver', arg: number): TPromise; - call(command: string, arg: any): TPromise; -} - -export class WindowDriverRegistryChannel implements IWindowDriverRegistryChannel { - - constructor(private registry: IWindowDriverRegistry) { } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); - case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg); - } - - return undefined; - } -} - -export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry { - - _serviceBrand: any; - - constructor(private channel: IWindowDriverRegistryChannel) { } - - registerWindowDriver(windowId: number): TPromise { - return this.channel.call('registerWindowDriver', windowId); - } - - reloadWindowDriver(windowId: number): TPromise { - return this.channel.call('reloadWindowDriver', windowId); - } -} - -export interface IWindowDriver { - click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; - doubleClick(selector: string): TPromise; - setValue(selector: string, text: string): TPromise; - getTitle(): TPromise; - isActiveElement(selector: string): TPromise; - getElements(selector: string, recursive: boolean): TPromise; - typeInEditor(selector: string, text: string): TPromise; - getTerminalBuffer(selector: string): TPromise; - writeInTerminal(selector: string, text: string): TPromise; -} - -export interface IWindowDriverChannel extends IChannel { - call(command: 'click', arg: [string, number | undefined, number | undefined]): TPromise; - call(command: 'doubleClick', arg: string): TPromise; - call(command: 'setValue', arg: [string, string]): TPromise; - call(command: 'getTitle'): TPromise; - call(command: 'isActiveElement', arg: string): TPromise; - call(command: 'getElements', arg: [string, boolean]): TPromise; - call(command: 'typeInEditor', arg: [string, string]): TPromise; - call(command: 'getTerminalBuffer', arg: string): TPromise; - call(command: 'writeInTerminal', arg: [string, string]): TPromise; - call(command: string, arg: any): TPromise; -} - -export class WindowDriverChannel implements IWindowDriverChannel { - - constructor(private driver: IWindowDriver) { } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'click': return this.driver.click(arg[0], arg[1], arg[2]); - case 'doubleClick': return this.driver.doubleClick(arg); - case 'setValue': return this.driver.setValue(arg[0], arg[1]); - case 'getTitle': return this.driver.getTitle(); - case 'isActiveElement': return this.driver.isActiveElement(arg); - case 'getElements': return this.driver.getElements(arg[0], arg[1]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); - } - - return undefined; - } -} - -export class WindowDriverChannelClient implements IWindowDriver { - - _serviceBrand: any; - - constructor(private channel: IWindowDriverChannel) { } - - click(selector: string, xoffset?: number, yoffset?: number): TPromise { - return this.channel.call('click', [selector, xoffset, yoffset]); - } - - doubleClick(selector: string): TPromise { - return this.channel.call('doubleClick', selector); - } - - setValue(selector: string, text: string): TPromise { - return this.channel.call('setValue', [selector, text]); - } - - getTitle(): TPromise { - return this.channel.call('getTitle'); - } - - isActiveElement(selector: string): TPromise { - return this.channel.call('isActiveElement', selector); - } - - getElements(selector: string, recursive: boolean): TPromise { - return this.channel.call('getElements', [selector, recursive]); - } - - typeInEditor(selector: string, text: string): TPromise { - return this.channel.call('typeInEditor', [selector, text]); - } - - getTerminalBuffer(selector: string): TPromise { - return this.channel.call('getTerminalBuffer', selector); - } - - writeInTerminal(selector: string, text: string): TPromise { - return this.channel.call('writeInTerminal', [selector, text]); - } -} \ No newline at end of file diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index 93e4cf36c80..90a76721c11 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -7,12 +7,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { IWindowDriver, IElement, WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driver'; -import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IWindowDriver, IElement, WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver'; +import { IPCClient } from 'vs/base/parts/ipc/node/ipc'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom'; import * as electron from 'electron'; import { IWindowService } from 'vs/platform/windows/common/windows'; +import { Terminal } from 'vscode-xterm'; +import { timeout } from 'vs/base/common/async'; function serializeElement(element: Element, recursive: boolean): IElement { const attributes = Object.create(null); @@ -49,7 +51,7 @@ class WindowDriver implements IWindowDriver { @IWindowService private windowService: IWindowService ) { } - async click(selector: string, xoffset?: number, yoffset?: number): TPromise { + click(selector: string, xoffset?: number, yoffset?: number): TPromise { return this._click(selector, 1, xoffset, yoffset); } @@ -57,11 +59,11 @@ class WindowDriver implements IWindowDriver { return this._click(selector, 2); } - private async _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> { + private _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error(`Element not found: ${selector}`)); } const { left, top } = getTopLeftOffset(element as HTMLElement); @@ -79,23 +81,27 @@ class WindowDriver implements IWindowDriver { x = Math.round(x); y = Math.round(y); - return { x, y }; + return TPromise.as({ x, y }); } - private async _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise { - const { x, y } = await this._getElementXY(selector, xoffset, yoffset); - const webContents = electron.remote.getCurrentWebContents(); - webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); - webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); + private _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise { + return this._getElementXY(selector, xoffset, yoffset).then(({ x, y }) => { - await TPromise.timeout(100); + const webContents: electron.WebContents = (electron as any).remote.getCurrentWebContents(); + webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); + + return TPromise.wrap(timeout(10)).then(() => { + webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); + return TPromise.wrap(timeout(100)); + }); + }); } - async setValue(selector: string, text: string): TPromise { + setValue(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error(`Element not found: ${selector}`)); } const inputElement = element as HTMLInputElement; @@ -103,13 +109,15 @@ class WindowDriver implements IWindowDriver { const event = new Event('input', { bubbles: true, cancelable: true }); inputElement.dispatchEvent(event); + + return TPromise.as(null); } - async getTitle(): TPromise { - return document.title; + getTitle(): TPromise { + return TPromise.as(document.title); } - async isActiveElement(selector: string): TPromise { + isActiveElement(selector: string): TPromise { const element = document.querySelector(selector); if (element !== document.activeElement) { @@ -125,13 +133,13 @@ class WindowDriver implements IWindowDriver { el = el.parentElement; } - throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'`); + return TPromise.wrapError(new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`)); } - return true; + return TPromise.as(true); } - async getElements(selector: string, recursive: boolean): TPromise { + getElements(selector: string, recursive: boolean): TPromise { const query = document.querySelectorAll(selector); const result: IElement[] = []; @@ -140,14 +148,14 @@ class WindowDriver implements IWindowDriver { result.push(serializeElement(element, recursive)); } - return result; + return TPromise.as(result); } - async typeInEditor(selector: string, text: string): TPromise { + typeInEditor(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Editor not found: ' + selector); + return TPromise.wrapError(new Error(`Editor not found: ${selector}`)); } const textarea = element as HTMLTextAreaElement; @@ -161,48 +169,52 @@ class WindowDriver implements IWindowDriver { const event = new Event('input', { 'bubbles': true, 'cancelable': true }); textarea.dispatchEvent(event); + + return TPromise.as(null); } - async getTerminalBuffer(selector: string): TPromise { + getTerminalBuffer(selector: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Terminal not found: ' + selector); + return TPromise.wrapError(new Error(`Terminal not found: ${selector}`)); } - const xterm = (element as any).xterm; + const xterm: Terminal = (element as any).xterm; if (!xterm) { - throw new Error('Xterm not found: ' + selector); + return TPromise.wrapError(new Error(`Xterm not found: ${selector}`)); } const lines: string[] = []; - for (let i = 0; i < xterm.buffer.lines.length; i++) { - lines.push(xterm.buffer.translateBufferLineToString(i, true)); + for (let i = 0; i < xterm._core.buffer.lines.length; i++) { + lines.push(xterm._core.buffer.translateBufferLineToString(i, true)); } - return lines; + return TPromise.as(lines); } - async writeInTerminal(selector: string, text: string): TPromise { + writeInTerminal(selector: string, text: string): TPromise { const element = document.querySelector(selector); if (!element) { - throw new Error('Element not found'); + return TPromise.wrapError(new Error(`Element not found: ${selector}`)); } - const xterm = (element as any).xterm; + const xterm: Terminal = (element as any).xterm; if (!xterm) { - throw new Error('Xterm not found'); + return TPromise.wrapError(new Error(`Xterm not found: ${selector}`)); } - xterm.send(text); + xterm._core.handler(text); + + return TPromise.as(null); } - async openDevTools(): TPromise { - await this.windowService.openDevTools({ mode: 'detach' }); + openDevTools(): TPromise { + return this.windowService.openDevTools({ mode: 'detach' }); } } @@ -210,7 +222,7 @@ export async function registerWindowDriver( client: IPCClient, windowId: number, instantiationService: IInstantiationService -): TPromise { +): Promise { const windowDriver = instantiationService.createInstance(WindowDriver); const windowDriverChannel = new WindowDriverChannel(windowDriver); client.registerChannel('windowDriver', windowDriverChannel); @@ -221,9 +233,9 @@ export async function registerWindowDriver( const options = await windowDriverRegistry.registerWindowDriver(windowId); if (options.verbose) { - // windowDriver.openDevTools(); + windowDriver.openDevTools(); } const disposable = toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId)); return combinedDisposable([disposable, client]); -} \ No newline at end of file +} diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 46efb22aed5..f448ede7a08 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -6,29 +6,31 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDriver, DriverChannel, IElement, IWindowDriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver, IDriverOptions } from 'vs/platform/driver/common/driver'; +import { IDriver, DriverChannel, IElement, IWindowDriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver, IDriverOptions } from 'vs/platform/driver/node/driver'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPCServer, IClientRouter } from 'vs/base/parts/ipc/common/ipc'; +import { IPCServer, IClientRouter } from 'vs/base/parts/ipc/node/ipc'; import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { OS } from 'vs/base/common/platform'; import { Emitter, toPromise } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; - -// TODO@joao: bad layering! -import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; -import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; -import { NativeImage } from 'electron'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; +import { timeout } from 'vs/base/common/async'; class WindowRouter implements IClientRouter { constructor(private windowId: number) { } - route(command: string, arg: any): string { - return `window:${this.windowId}`; + routeCall(): TPromise { + return TPromise.as(`window:${this.windowId}`); + } + + routeEvent(): TPromise { + return TPromise.as(`window:${this.windowId}`); } } @@ -50,57 +52,57 @@ export class Driver implements IDriver, IWindowDriverRegistry { @IWindowsMainService private windowsService: IWindowsMainService ) { } - async registerWindowDriver(windowId: number): TPromise { + registerWindowDriver(windowId: number): TPromise { this.registeredWindowIds.add(windowId); this.reloadingWindowIds.delete(windowId); this.onDidReloadingChange.fire(); - return this.options; + return TPromise.as(this.options); } - async reloadWindowDriver(windowId: number): TPromise { + reloadWindowDriver(windowId: number): TPromise { this.reloadingWindowIds.add(windowId); + return TPromise.as(null); } - async getWindowIds(): TPromise { - return this.windowsService.getWindows() + getWindowIds(): TPromise { + return TPromise.as(this.windowsService.getWindows() .map(w => w.id) - .filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id)); + .filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id))); } - async capturePage(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const window = this.windowsService.getWindowById(windowId); - const webContents = window.win.webContents; - const image = await new Promise(c => webContents.capturePage(c)); - const buffer = image.toPNG(); - - return buffer.toString('base64'); + capturePage(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const window = this.windowsService.getWindowById(windowId); + const webContents = window.win.webContents; + return new TPromise(c => webContents.capturePage(image => c(image.toPNG().toString('base64')))); + }); } - async reloadWindow(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const window = this.windowsService.getWindowById(windowId); - this.reloadingWindowIds.add(windowId); - this.windowsService.reload(window); + reloadWindow(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const window = this.windowsService.getWindowById(windowId); + this.reloadingWindowIds.add(windowId); + this.windowsService.reload(window); + }); } - async dispatchKeybinding(windowId: number, keybinding: string): TPromise { - await this.whenUnfrozen(windowId); + dispatchKeybinding(windowId: number, keybinding: string): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const [first, second] = KeybindingParser.parseUserBinding(keybinding); - const [first, second] = KeybindingIO._readUserBinding(keybinding); - - await this._dispatchKeybinding(windowId, first); - - if (second) { - await this._dispatchKeybinding(windowId, second); - } + return this._dispatchKeybinding(windowId, first).then(() => { + if (second) { + return this._dispatchKeybinding(windowId, second); + } else { + return TPromise.as(null); + } + }); + }); } - private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise { + private _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise { if (keybinding instanceof ScanCodeBinding) { - throw new Error('ScanCodeBindings not supported'); + return TPromise.wrapError(new Error('ScanCodeBindings not supported')); } const window = this.windowsService.getWindowById(windowId); @@ -135,63 +137,76 @@ export class Driver implements IDriver, IWindowDriverRegistry { webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any); - await TPromise.timeout(100); + return TPromise.wrap(timeout(100)); } - async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.click(selector, xoffset, yoffset); + click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.click(selector, xoffset, yoffset); + }); } - async doubleClick(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.doubleClick(selector); + doubleClick(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.doubleClick(selector); + }); } - async setValue(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.setValue(selector, text); + setValue(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.setValue(selector, text); + }); } - async getTitle(windowId: number): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getTitle(); + getTitle(windowId: number): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getTitle(); + }); } - async isActiveElement(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.isActiveElement(selector); + isActiveElement(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.isActiveElement(selector); + }); } - async getElements(windowId: number, selector: string, recursive: boolean): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getElements(selector, recursive); + getElements(windowId: number, selector: string, recursive: boolean): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getElements(selector, recursive); + }); } - async typeInEditor(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.typeInEditor(selector, text); + typeInEditor(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.typeInEditor(selector, text); + }); } - async getTerminalBuffer(windowId: number, selector: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.getTerminalBuffer(selector); + getTerminalBuffer(windowId: number, selector: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.getTerminalBuffer(selector); + }); } - async writeInTerminal(windowId: number, selector: string, text: string): TPromise { - const windowDriver = await this.getWindowDriver(windowId); - return windowDriver.writeInTerminal(selector, text); + writeInTerminal(windowId: number, selector: string, text: string): TPromise { + return this.getWindowDriver(windowId).then(windowDriver => { + return windowDriver.writeInTerminal(selector, text); + }); } - private async getWindowDriver(windowId: number): TPromise { - await this.whenUnfrozen(windowId); - - const router = new WindowRouter(windowId); - const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); - return new WindowDriverChannelClient(windowDriverChannel); + private getWindowDriver(windowId: number): TPromise { + return this.whenUnfrozen(windowId).then(() => { + const router = new WindowRouter(windowId); + const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); + return new WindowDriverChannelClient(windowDriverChannel); + }); } - private async whenUnfrozen(windowId: number): TPromise { + private whenUnfrozen(windowId: number): TPromise { + return TPromise.wrap(this._whenUnfrozen(windowId)); + } + + private async _whenUnfrozen(windowId: number): Promise { while (this.reloadingWindowIds.has(windowId)) { await toPromise(this.onDidReloadingChange.event); } @@ -203,7 +218,7 @@ export async function serve( handle: string, environmentService: IEnvironmentService, instantiationService: IInstantiationService -): TPromise { +): Promise { const verbose = environmentService.driverVerbose; const driver = instantiationService.createInstance(Driver, windowServer, { verbose }); @@ -215,4 +230,4 @@ export async function serve( server.registerChannel('driver', channel); return combinedDisposable([server, windowServer]); -} \ No newline at end of file +} diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index fb61d1d1fa6..2195bcb9a67 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -5,11 +5,294 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IDriver, DriverChannelClient } from 'vs/platform/driver/common/driver'; import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { Event } from 'vs/base/common/event'; -export async function connect(handle: string): TPromise<{ client: Client, driver: IDriver }> { +export const ID = 'driverService'; +export const IDriver = createDecorator(ID); + +// !! Do not remove the following START and END markers, they are parsed by the smoketest build + +//*START +export interface IElement { + tagName: string; + className: string; + textContent: string; + attributes: { [name: string]: string; }; + children: IElement[]; + top: number; + left: number; +} + +export interface IDriver { + _serviceBrand: any; + + getWindowIds(): TPromise; + capturePage(windowId: number): TPromise; + reloadWindow(windowId: number): TPromise; + dispatchKeybinding(windowId: number, keybinding: string): TPromise; + click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; + doubleClick(windowId: number, selector: string): TPromise; + setValue(windowId: number, selector: string, text: string): TPromise; + getTitle(windowId: number): TPromise; + isActiveElement(windowId: number, selector: string): TPromise; + getElements(windowId: number, selector: string, recursive?: boolean): TPromise; + typeInEditor(windowId: number, selector: string, text: string): TPromise; + getTerminalBuffer(windowId: number, selector: string): TPromise; + writeInTerminal(windowId: number, selector: string, text: string): TPromise; +} +//*END + +export interface IDriverChannel extends IChannel { + call(command: 'getWindowIds'): Thenable; + call(command: 'capturePage'): Thenable; + call(command: 'reloadWindow', arg: number): Thenable; + call(command: 'dispatchKeybinding', arg: [number, string]): Thenable; + call(command: 'click', arg: [number, string, number | undefined, number | undefined]): Thenable; + call(command: 'doubleClick', arg: [number, string]): Thenable; + call(command: 'setValue', arg: [number, string, string]): Thenable; + call(command: 'getTitle', arg: [number]): Thenable; + call(command: 'isActiveElement', arg: [number, string]): Thenable; + call(command: 'getElements', arg: [number, string, boolean]): Thenable; + call(command: 'typeInEditor', arg: [number, string, string]): Thenable; + call(command: 'getTerminalBuffer', arg: [number, string]): Thenable; + call(command: 'writeInTerminal', arg: [number, string, string]): Thenable; + call(command: string, arg: any): Thenable; +} + +export class DriverChannel implements IDriverChannel { + + constructor(private driver: IDriver) { } + + listen(event: string): Event { + throw new Error('No event found'); + } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'getWindowIds': return this.driver.getWindowIds(); + case 'capturePage': return this.driver.capturePage(arg); + case 'reloadWindow': return this.driver.reloadWindow(arg); + case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); + case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); + case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]); + case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); + case 'getTitle': return this.driver.getTitle(arg[0]); + case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); + case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); + case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); + case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); + case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); + } + + return undefined; + } +} + +export class DriverChannelClient implements IDriver { + + _serviceBrand: any; + + constructor(private channel: IDriverChannel) { } + + getWindowIds(): TPromise { + return TPromise.wrap(this.channel.call('getWindowIds')); + } + + capturePage(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('capturePage', windowId)); + } + + reloadWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('reloadWindow', windowId)); + } + + dispatchKeybinding(windowId: number, keybinding: string): TPromise { + return TPromise.wrap(this.channel.call('dispatchKeybinding', [windowId, keybinding])); + } + + click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): TPromise { + return TPromise.wrap(this.channel.call('click', [windowId, selector, xoffset, yoffset])); + } + + doubleClick(windowId: number, selector: string): TPromise { + return TPromise.wrap(this.channel.call('doubleClick', [windowId, selector])); + } + + setValue(windowId: number, selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('setValue', [windowId, selector, text])); + } + + getTitle(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('getTitle', [windowId])); + } + + isActiveElement(windowId: number, selector: string): TPromise { + return TPromise.wrap(this.channel.call('isActiveElement', [windowId, selector])); + } + + getElements(windowId: number, selector: string, recursive: boolean): TPromise { + return TPromise.wrap(this.channel.call('getElements', [windowId, selector, recursive])); + } + + typeInEditor(windowId: number, selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('typeInEditor', [windowId, selector, text])); + } + + getTerminalBuffer(windowId: number, selector: string): TPromise { + return TPromise.wrap(this.channel.call('getTerminalBuffer', [windowId, selector])); + } + + writeInTerminal(windowId: number, selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('writeInTerminal', [windowId, selector, text])); + } +} + +export interface IDriverOptions { + verbose: boolean; +} + +export interface IWindowDriverRegistry { + registerWindowDriver(windowId: number): TPromise; + reloadWindowDriver(windowId: number): TPromise; +} + +export interface IWindowDriverRegistryChannel extends IChannel { + call(command: 'registerWindowDriver', arg: number): Thenable; + call(command: 'reloadWindowDriver', arg: number): Thenable; + call(command: string, arg: any): Thenable; +} + +export class WindowDriverRegistryChannel implements IWindowDriverRegistryChannel { + + constructor(private registry: IWindowDriverRegistry) { } + + listen(event: string): Event { + throw new Error('No event found'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); + case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg); + } + + return undefined; + } +} + +export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry { + + _serviceBrand: any; + + constructor(private channel: IWindowDriverRegistryChannel) { } + + registerWindowDriver(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('registerWindowDriver', windowId)); + } + + reloadWindowDriver(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('reloadWindowDriver', windowId)); + } +} + +export interface IWindowDriver { + click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise; + doubleClick(selector: string): TPromise; + setValue(selector: string, text: string): TPromise; + getTitle(): TPromise; + isActiveElement(selector: string): TPromise; + getElements(selector: string, recursive: boolean): TPromise; + typeInEditor(selector: string, text: string): TPromise; + getTerminalBuffer(selector: string): TPromise; + writeInTerminal(selector: string, text: string): TPromise; +} + +export interface IWindowDriverChannel extends IChannel { + call(command: 'click', arg: [string, number | undefined, number | undefined]): Thenable; + call(command: 'doubleClick', arg: string): Thenable; + call(command: 'setValue', arg: [string, string]): Thenable; + call(command: 'getTitle'): Thenable; + call(command: 'isActiveElement', arg: string): Thenable; + call(command: 'getElements', arg: [string, boolean]): Thenable; + call(command: 'typeInEditor', arg: [string, string]): Thenable; + call(command: 'getTerminalBuffer', arg: string): Thenable; + call(command: 'writeInTerminal', arg: [string, string]): Thenable; + call(command: string, arg: any): Thenable; +} + +export class WindowDriverChannel implements IWindowDriverChannel { + + constructor(private driver: IWindowDriver) { } + + listen(event: string): Event { + throw new Error('No event found'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'click': return this.driver.click(arg[0], arg[1], arg[2]); + case 'doubleClick': return this.driver.doubleClick(arg); + case 'setValue': return this.driver.setValue(arg[0], arg[1]); + case 'getTitle': return this.driver.getTitle(); + case 'isActiveElement': return this.driver.isActiveElement(arg); + case 'getElements': return this.driver.getElements(arg[0], arg[1]); + case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); + case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); + case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); + } + + return undefined; + } +} + +export class WindowDriverChannelClient implements IWindowDriver { + + _serviceBrand: any; + + constructor(private channel: IWindowDriverChannel) { } + + click(selector: string, xoffset?: number, yoffset?: number): TPromise { + return TPromise.wrap(this.channel.call('click', [selector, xoffset, yoffset])); + } + + doubleClick(selector: string): TPromise { + return TPromise.wrap(this.channel.call('doubleClick', selector)); + } + + setValue(selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('setValue', [selector, text])); + } + + getTitle(): TPromise { + return TPromise.wrap(this.channel.call('getTitle')); + } + + isActiveElement(selector: string): TPromise { + return TPromise.wrap(this.channel.call('isActiveElement', selector)); + } + + getElements(selector: string, recursive: boolean): TPromise { + return TPromise.wrap(this.channel.call('getElements', [selector, recursive])); + } + + typeInEditor(selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('typeInEditor', [selector, text])); + } + + getTerminalBuffer(selector: string): TPromise { + return TPromise.wrap(this.channel.call('getTerminalBuffer', selector)); + } + + writeInTerminal(selector: string, text: string): TPromise { + return TPromise.wrap(this.channel.call('writeInTerminal', [selector, text])); + } +} + +export async function connect(handle: string): Promise<{ client: Client, driver: IDriver }> { const client = await connectNet(handle, 'driverClient'); const channel = client.getChannel('driver'); const driver = new DriverChannelClient(channel); diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index d35396a181a..40d7169e10b 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event } from 'vs/base/common/event'; @@ -42,6 +42,15 @@ export interface IBaseResourceInput { * Description to show for the diff editor */ description?: string; + + /** + * Hint to indicate that this input should be treated as a file + * that opens in an editor capable of showing file content. + * + * Without this hint, the editor service will make a guess by + * looking at the scheme of the resource(s). + */ + forceFile?: boolean; } export interface IResourceInput extends IBaseResourceInput { @@ -66,11 +75,11 @@ export interface IEditorOptions { readonly preserveFocus?: boolean; /** - * Tells the editor to replace the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not replace the input if it is identical to the + * Tells the editor to reload the editor input in the editor even if it is identical to the one + * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - readonly forceOpen?: boolean; + readonly forceReload?: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. Note diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 06f80764726..d81317dc247 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; export interface ParsedArgs { [arg: string]: any; _: string[]; + 'folder-uri'?: string | string[]; + 'file-uri'?: string | string[]; _urls?: string[]; help?: boolean; version?: boolean; @@ -25,11 +28,12 @@ export interface ParsedArgs { performance?: boolean; 'prof-startup'?: string; 'prof-startup-prefix'?: string; + 'prof-append-timers'?: string; verbose?: boolean; log?: string; logExtensionHostCommunication?: boolean; - 'disable-extensions'?: boolean; 'extensions-dir'?: string; + 'builtin-extensions-dir'?: string; extensionDevelopmentPath?: string; extensionTestsPath?: string; debugPluginHost?: string; @@ -37,6 +41,8 @@ export interface ParsedArgs { debugId?: string; debugSearch?: string; debugBrkSearch?: string; + 'disable-extensions'?: boolean; + 'disable-extension'?: string | string[]; 'list-extensions'?: boolean; 'show-versions'?: boolean; 'install-extension'?: string | string[]; @@ -99,9 +105,10 @@ export interface IEnvironmentService { workspacesHome: string; isExtensionDevelopment: boolean; - disableExtensions: boolean; + disableExtensions: boolean | string[]; + builtinExtensionsPath: string; extensionsPath: string; - extensionDevelopmentPath: string; + extensionDevelopmentLocationURI: URI; extensionTestsPath: string; debugExtensionHost: IExtensionHostDebugParams; @@ -116,6 +123,7 @@ export interface IEnvironmentService { performance: boolean; // logging + log: string; logsPath: string; verbose: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 529890d3034..7183c2471eb 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -18,9 +18,12 @@ const options: minimist.Opts = { 'locale', 'user-data-dir', 'extensions-dir', + 'folder-uri', + 'file-uri', 'extensionDevelopmentPath', 'extensionTestsPath', 'install-extension', + 'disable-extension', 'uninstall-extension', 'debugId', 'debugPluginHost', @@ -146,7 +149,7 @@ const optionsHelp: { [name: string]: string; } = { '-a, --add ': localize('add', "Add folder(s) to the last active window."), '-g, --goto ': localize('goto', "Open a file at the path on the specified line and character position."), '-n, --new-window': localize('newWindow', "Force to open a new window."), - '-r, --reuse-window': localize('reuseWindow', "Force to open a file or folder in the last active window."), + '-r, --reuse-window': localize('reuseWindow', "Force to open a file or folder in an already opened window."), '-w, --wait': localize('wait', "Wait for the files to be closed before returning."), '--locale ': localize('locale', "The locale to use (e.g. en-US or zh-TW)."), '--user-data-dir ': localize('userDataDir', "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code."), @@ -160,7 +163,7 @@ const extensionsHelp: { [name: string]: string; } = { '--show-versions': localize('showVersions', "Show versions of installed extensions, when using --list-extension."), '--install-extension ( | )': localize('installExtension', "Installs an extension."), '--uninstall-extension ( | )': localize('uninstallExtension', "Uninstalls an extension."), - '--enable-proposed-api ': localize('experimentalApis', "Enables proposed API features for an extension.") + '--enable-proposed-api ()': localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }; const troubleshootingHelp: { [name: string]: string; } = { @@ -170,6 +173,7 @@ const troubleshootingHelp: { [name: string]: string; } = { '-p, --performance': localize('performance', "Start with the 'Developer: Startup Performance' command enabled."), '--prof-startup': localize('prof-startup', "Run CPU profiler during startup"), '--disable-extensions': localize('disableExtensions', "Disable all installed extensions."), + '--disable-extension ': localize('disableExtension', "Disable an extension."), '--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI."), '--inspect-brk-extensions': localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI."), '--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."), @@ -230,3 +234,30 @@ ${formatOptions(extensionsHelp, columns)} ${ localize('troubleshooting', "Troubleshooting")}: ${formatOptions(troubleshootingHelp, columns)}`; } + +/** + * Converts an argument into an array + * @param arg a argument value. Can be undefined, an entry or an array + */ +export function asArray(arg: string | string[] | undefined): string[] { + if (arg) { + if (Array.isArray(arg)) { + return arg; + } + return [arg]; + } + return []; +} + +/** + * Returns whether an argument is present. + */ +export function hasArgs(arg: string | string[] | undefined): boolean { + if (arg) { + if (Array.isArray(arg)) { + return !!arg.length; + } + return true; + } + return false; +} \ No newline at end of file diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 381202a6d1b..7d7aacfc48f 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -8,12 +8,13 @@ import * as crypto from 'crypto'; import * as paths from 'vs/base/node/paths'; import * as os from 'os'; import * as path from 'path'; -import URI from 'vs/base/common/uri'; import { memoize } from 'vs/base/common/decorators'; import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; import { toLocalISOString } from 'vs/base/common/date'; import { isWindows, isLinux } from 'vs/base/common/platform'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { URI } from 'vs/base/common/uri'; // Read this before there's any chance it is overwritten // Related to https://github.com/Microsoft/vscode/issues/30624 @@ -77,7 +78,7 @@ export class EnvironmentService implements IEnvironmentService { get args(): ParsedArgs { return this._args; } @memoize - get appRoot(): string { return path.dirname(URI.parse(require.toUrl('')).fsPath); } + get appRoot(): string { return path.dirname(getPathFromAmdModule(require, '')); } get execPath(): string { return this._execPath; } @@ -90,7 +91,13 @@ export class EnvironmentService implements IEnvironmentService { get userHome(): string { return os.homedir(); } @memoize - get userDataPath(): string { return parseUserDataDir(this._args, process); } + get userDataPath(): string { + if (process.env['VSCODE_PORTABLE']) { + return path.join(process.env['VSCODE_PORTABLE'], 'user-data'); + } + + return parseUserDataDir(this._args, process); + } get appNameLong(): string { return product.nameLong; } @@ -127,15 +134,60 @@ export class EnvironmentService implements IEnvironmentService { get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); } @memoize - get extensionsPath(): string { return parsePathArg(this._args['extensions-dir'], process) || process.env['VSCODE_EXTENSIONS'] || path.join(this.userHome, product.dataFolderName, 'extensions'); } + get builtinExtensionsPath(): string { + const fromArgs = parsePathArg(this._args['builtin-extensions-dir'], process); + if (fromArgs) { + return fromArgs; + } else { + return path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'extensions')); + } + } @memoize - get extensionDevelopmentPath(): string { return this._args.extensionDevelopmentPath ? path.normalize(this._args.extensionDevelopmentPath) : this._args.extensionDevelopmentPath; } + get extensionsPath(): string { + const fromArgs = parsePathArg(this._args['extensions-dir'], process); + + if (fromArgs) { + return fromArgs; + } else if (process.env['VSCODE_EXTENSIONS']) { + return process.env['VSCODE_EXTENSIONS']; + } else if (process.env['VSCODE_PORTABLE']) { + return path.join(process.env['VSCODE_PORTABLE'], 'extensions'); + } else { + return path.join(this.userHome, product.dataFolderName, 'extensions'); + } + } + + @memoize + get extensionDevelopmentLocationURI(): URI { + const s = this._args.extensionDevelopmentPath; + if (s) { + if (/^[^:/?#]+?:\/\//.test(s)) { + return URI.parse(s); + } + return URI.file(path.normalize(s)); + } + return void 0; + } @memoize get extensionTestsPath(): string { return this._args.extensionTestsPath ? path.normalize(this._args.extensionTestsPath) : this._args.extensionTestsPath; } - get disableExtensions(): boolean { return this._args['disable-extensions']; } + get disableExtensions(): boolean | string[] { + if (this._args['disable-extensions']) { + return true; + } + const disableExtensions: string | string[] = this._args['disable-extension']; + if (disableExtensions) { + if (typeof disableExtensions === 'string') { + return [disableExtensions]; + } + if (Array.isArray(disableExtensions) && disableExtensions.length > 0) { + return disableExtensions; + } + } + return false; + } get skipGettingStarted(): boolean { return this._args['skip-getting-started']; } @@ -151,6 +203,7 @@ export class EnvironmentService implements IEnvironmentService { get isBuilt(): boolean { return !process.env['VSCODE_DEV']; } get verbose(): boolean { return this._args.verbose; } + get log(): string { return this._args.log; } get wait(): boolean { return this._args.wait; } get logExtensionHostCommunication(): boolean { return this._args.logExtensionHostCommunication; } diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 28c6ce6805f..f56f3aabbbf 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, ILocalExtension, isIExtensionIdentifier, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getIdFromLocalExtensionId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getIdFromLocalExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,7 +29,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { @IStorageService private storageService: IStorageService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IEnvironmentService private environmentService: IEnvironmentService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService + @IExtensionManagementService private extensionManagementService: IExtensionManagementService ) { extensionManagementService.onDidUninstallExtension(this._onDidUninstallExtension, this, this.disposables); } @@ -38,7 +38,11 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; } - getDisabledExtensions(): TPromise { + get allUserExtensionsDisabled(): boolean { + return this.environmentService.disableExtensions === true; + } + + async getDisabledExtensions(): Promise { let result = this._getDisabledExtensions(StorageScope.GLOBAL); @@ -54,14 +58,25 @@ export class ExtensionEnablementService implements IExtensionEnablementService { } } - return TPromise.as(result); + if (this.environmentService.disableExtensions) { + const allInstalledExtensions = await this.extensionManagementService.getInstalled(); + for (const installedExtension of allInstalledExtensions) { + if (this._isExtensionDisabledInEnvironment(installedExtension)) { + if (!result.some(r => areSameExtensions(r, installedExtension.galleryIdentifier))) { + result.push(installedExtension.galleryIdentifier); + } + } + } + } + + return result; } getEnablementState(extension: ILocalExtension): EnablementState { - if (this.environmentService.disableExtensions && extension.type === LocalExtensionType.User) { + if (this._isExtensionDisabledInEnvironment(extension)) { return EnablementState.Disabled; } - const identifier = this._getIdentifier(extension); + const identifier = extension.galleryIdentifier; if (this.hasWorkspace) { if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) { return EnablementState.WorkspaceEnabled; @@ -95,7 +110,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { if (!this.canChangeEnablement(arg)) { return TPromise.wrap(false); } - identifier = this._getIdentifier(arg); + identifier = arg.galleryIdentifier; } const workspace = newState === EnablementState.WorkspaceDisabled || newState === EnablementState.WorkspaceEnabled; @@ -134,6 +149,17 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return enablementState === EnablementState.WorkspaceEnabled || enablementState === EnablementState.Enabled; } + private _isExtensionDisabledInEnvironment(extension: ILocalExtension): boolean { + if (this.allUserExtensionsDisabled) { + return extension.type === LocalExtensionType.User; + } + const disabledExtensions = this.environmentService.disableExtensions; + if (Array.isArray(disabledExtensions)) { + return disabledExtensions.some(id => areSameExtensions({ id }, extension.galleryIdentifier)); + } + return false; + } + private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { if (this.hasWorkspace) { if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) { @@ -150,10 +176,6 @@ export class ExtensionEnablementService implements IExtensionEnablementService { return EnablementState.Enabled; } - private _getIdentifier(extension: ILocalExtension): IExtensionIdentifier { - return { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; - } - private _enableExtension(identifier: IExtensionIdentifier): void { this._removeFromDisabledExtensions(identifier, StorageScope.WORKSPACE); this._removeFromEnabledExtensions(identifier, StorageScope.WORKSPACE); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index fc191890180..9a79972905f 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -11,6 +11,9 @@ import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILocalization } from 'vs/platform/localizations/common/localizations'; +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); @@ -94,7 +97,7 @@ export interface IColor { export interface IExtensionContributions { commands?: ICommand[]; - configuration?: IConfiguration; + configuration?: IConfiguration | IConfiguration[]; debuggers?: IDebugger[]; grammars?: IGrammar[]; jsonValidation?: IJSONValidation[]; @@ -120,8 +123,10 @@ export interface IExtensionManifest { main?: string; icon?: string; categories?: string[]; + keywords?: string[]; activationEvents?: string[]; extensionDependencies?: string[]; + extensionPack?: string[]; contributes?: IExtensionContributions; repository?: { url: string; @@ -133,6 +138,7 @@ export interface IExtensionManifest { export interface IGalleryExtensionProperties { dependencies?: string[]; + extensionPack?: string[]; engine?: string; } @@ -195,7 +201,7 @@ export interface IGalleryMetadata { publisherDisplayName: string; } -export enum LocalExtensionType { +export const enum LocalExtensionType { System, User } @@ -203,9 +209,10 @@ export enum LocalExtensionType { export interface ILocalExtension { type: LocalExtensionType; identifier: IExtensionIdentifier; + galleryIdentifier: IExtensionIdentifier; manifest: IExtensionManifest; metadata: IGalleryMetadata; - path: string; + location: URI; readmeUrl: string; changelogUrl: string; } @@ -213,7 +220,7 @@ export interface ILocalExtension { export const IExtensionManagementService = createDecorator('extensionManagementService'); export const IExtensionGalleryService = createDecorator('extensionGalleryService'); -export enum SortBy { +export const enum SortBy { NoneOrRelevance = 0, LastUpdatedDate = 1, Title = 2, @@ -224,7 +231,7 @@ export enum SortBy { WeightedRating = 12 } -export enum SortOrder { +export const enum SortOrder { Default = 0, Ascending = 1, Descending = 2 @@ -240,7 +247,7 @@ export interface IQueryOptions { source?: string; } -export enum StatisticType { +export const enum StatisticType { Uninstall = 'uninstall' } @@ -249,8 +256,9 @@ export interface IReportedExtension { malicious: boolean; } -export enum InstallOperation { - Install = 1, +export const enum InstallOperation { + None = 0, + Install, Update } @@ -264,13 +272,14 @@ export interface IExtensionGalleryService { query(options?: IQueryOptions): TPromise>; download(extension: IGalleryExtension, operation: InstallOperation): TPromise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): TPromise; - getReadme(extension: IGalleryExtension): TPromise; - getManifest(extension: IGalleryExtension): TPromise; - getChangelog(extension: IGalleryExtension): TPromise; + getReadme(extension: IGalleryExtension, token: CancellationToken): TPromise; + getManifest(extension: IGalleryExtension, token: CancellationToken): TPromise; + getChangelog(extension: IGalleryExtension, token: CancellationToken): TPromise; getCoreTranslation(extension: IGalleryExtension, languageId: string): TPromise; loadCompatibleVersion(extension: IGalleryExtension): TPromise; - loadAllDependencies(dependencies: IExtensionIdentifier[]): TPromise; + loadAllDependencies(dependencies: IExtensionIdentifier[], token: CancellationToken): TPromise; getExtensionsReport(): TPromise; + getExtension(id: IExtensionIdentifier, version?: string): TPromise; } export interface InstallExtensionEvent { @@ -301,17 +310,34 @@ export interface IExtensionManagementService { onUninstallExtension: Event; onDidUninstallExtension: Event; - install(zipPath: string): TPromise; - installFromGallery(extension: IGalleryExtension): TPromise; + zip(extension: ILocalExtension): TPromise; + unzip(zipLocation: URI, type: LocalExtensionType): TPromise; + install(vsix: URI): TPromise; + installFromGallery(extension: IGalleryExtension): TPromise; uninstall(extension: ILocalExtension, force?: boolean): TPromise; - reinstallFromGallery(extension: ILocalExtension): TPromise; + reinstallFromGallery(extension: ILocalExtension): TPromise; getInstalled(type?: LocalExtensionType): TPromise; getExtensionsReport(): TPromise; updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise; } -export enum EnablementState { +export const IExtensionManagementServerService = createDecorator('extensionManagementServerService'); + +export interface IExtensionManagementServer { + extensionManagementService: IExtensionManagementService; + authority: string; + label: string; +} + +export interface IExtensionManagementServerService { + _serviceBrand: any; + readonly extensionManagementServers: IExtensionManagementServer[]; + getLocalExtensionManagementServer(): IExtensionManagementServer; + getExtensionManagementServer(location: URI): IExtensionManagementServer; +} + +export const enum EnablementState { Disabled, WorkspaceDisabled, Enabled, @@ -324,6 +350,8 @@ export const IExtensionEnablementService = createDecorator; + getDisabledExtensions(): Promise; /** * Returns the enablement state for the given extension @@ -362,24 +390,49 @@ export interface IExtensionEnablementService { setEnablement(extension: ILocalExtension, state: EnablementState): TPromise; } +export interface IExtensionsConfigContent { + recommendations: string[]; + unwantedRecommendations: string[]; +} + +export type RecommendationChangeNotification = { + extensionId: string, + isRecommended: boolean +}; + +export type DynamicRecommendation = 'dynamic'; +export type ExecutableRecommendation = 'executable'; +export type CachedRecommendation = 'cached'; +export type ApplicationRecommendation = 'application'; +export type ExtensionRecommendationSource = IWorkspace | IWorkspaceFolder | URI | DynamicRecommendation | ExecutableRecommendation | CachedRecommendation | ApplicationRecommendation; + +export interface IExtensionRecommendation { + extensionId: string; + sources: ExtensionRecommendationSource[]; +} + export const IExtensionTipsService = createDecorator('extensionTipsService'); export interface IExtensionTipsService { _serviceBrand: any; getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; }; - getFileBasedRecommendations(): string[]; - getOtherRecommendations(): TPromise; - getWorkspaceRecommendations(): TPromise; - getKeymapRecommendations(): string[]; + getFileBasedRecommendations(): IExtensionRecommendation[]; + getOtherRecommendations(): TPromise; + getWorkspaceRecommendations(): TPromise; + getKeymapRecommendations(): IExtensionRecommendation[]; + getAllRecommendations(): TPromise; getKeywordsForExtension(extension: string): string[]; - getRecommendationsForExtension(extension: string): string[]; + toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void; + getAllIgnoredRecommendations(): { global: string[], workspace: string[] }; + onRecommendationChange: Event; } -export enum ExtensionRecommendationReason { +export const enum ExtensionRecommendationReason { Workspace, File, Executable, - DynamicWorkspace + DynamicWorkspace, + Experimental } export const ExtensionsLabel = localize('extensions', "Extensions"); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts deleted file mode 100644 index b54d3db67f2..00000000000 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, LocalExtensionType, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from './extensionManagement'; -import { Event, buffer } from 'vs/base/common/event'; - -export interface IExtensionManagementChannel extends IChannel { - call(command: 'event:onInstallExtension'): TPromise; - call(command: 'event:onDidInstallExtension'): TPromise; - call(command: 'event:onUninstallExtension'): TPromise; - call(command: 'event:onDidUninstallExtension'): TPromise; - call(command: 'install', path: string): TPromise; - call(command: 'installFromGallery', extension: IGalleryExtension): TPromise; - call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise; - call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; - call(command: 'getInstalled'): TPromise; - call(command: 'getExtensionsReport'): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class ExtensionManagementChannel implements IExtensionManagementChannel { - - onInstallExtension: Event; - onDidInstallExtension: Event; - onUninstallExtension: Event; - onDidUninstallExtension: Event; - - constructor(private service: IExtensionManagementService) { - this.onInstallExtension = buffer(service.onInstallExtension, true); - this.onDidInstallExtension = buffer(service.onDidInstallExtension, true); - this.onUninstallExtension = buffer(service.onUninstallExtension, true); - this.onDidUninstallExtension = buffer(service.onDidUninstallExtension, true); - } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'event:onInstallExtension': return eventToCall(this.onInstallExtension); - case 'event:onDidInstallExtension': return eventToCall(this.onDidInstallExtension); - case 'event:onUninstallExtension': return eventToCall(this.onUninstallExtension); - case 'event:onDidUninstallExtension': return eventToCall(this.onDidUninstallExtension); - case 'install': return this.service.install(arg); - case 'installFromGallery': return this.service.installFromGallery(arg[0]); - case 'uninstall': return this.service.uninstall(arg[0], arg[1]); - case 'reinstallFromGallery': return this.service.reinstallFromGallery(arg[0]); - case 'getInstalled': return this.service.getInstalled(arg); - case 'updateMetadata': return this.service.updateMetadata(arg[0], arg[1]); - case 'getExtensionsReport': return this.service.getExtensionsReport(); - } - return undefined; - } -} - -export class ExtensionManagementChannelClient implements IExtensionManagementService { - - _serviceBrand: any; - - constructor(private channel: IExtensionManagementChannel) { } - - private _onInstallExtension = eventFromCall(this.channel, 'event:onInstallExtension'); - get onInstallExtension(): Event { return this._onInstallExtension; } - - private _onDidInstallExtension = eventFromCall(this.channel, 'event:onDidInstallExtension'); - get onDidInstallExtension(): Event { return this._onDidInstallExtension; } - - private _onUninstallExtension = eventFromCall(this.channel, 'event:onUninstallExtension'); - get onUninstallExtension(): Event { return this._onUninstallExtension; } - - private _onDidUninstallExtension = eventFromCall(this.channel, 'event:onDidUninstallExtension'); - get onDidUninstallExtension(): Event { return this._onDidUninstallExtension; } - - install(zipPath: string): TPromise { - return this.channel.call('install', zipPath); - } - - installFromGallery(extension: IGalleryExtension): TPromise { - return this.channel.call('installFromGallery', [extension]); - } - - uninstall(extension: ILocalExtension, force = false): TPromise { - return this.channel.call('uninstall', [extension, force]); - } - - reinstallFromGallery(extension: ILocalExtension): TPromise { - return this.channel.call('reinstallFromGallery', [extension]); - } - - getInstalled(type: LocalExtensionType = null): TPromise { - return this.channel.call('getInstalled', type); - } - - updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise { - return this.channel.call('updateMetadata', [local, metadata]); - } - - getExtensionsReport(): TPromise { - return this.channel.call('getExtensionsReport'); - } -} \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 505673fa7fc..2e989c69d67 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -5,7 +5,9 @@ 'use strict'; -import { ILocalExtension, IGalleryExtension, EXTENSION_IDENTIFIER_REGEX, IExtensionIdentifier, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IReportedExtension, IExtensionManifest } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { compareIgnoreCase } from 'vs/base/common/strings'; export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean { if (a.uuid && b.uuid) { @@ -14,7 +16,11 @@ export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifi if (a.id === b.id) { return true; } - return adoptToGalleryExtensionId(a.id) === adoptToGalleryExtensionId(b.id); + return compareIgnoreCase(a.id, b.id) === 0; +} + +export function adoptToGalleryExtensionId(id: string): string { + return id.toLocaleLowerCase(); } export function getGalleryExtensionId(publisher: string, name: string): string { @@ -35,10 +41,6 @@ export function getIdFromLocalExtensionId(localExtensionId: string): string { return adoptToGalleryExtensionId(localExtensionId); } -export function adoptToGalleryExtensionId(id: string): string { - return id.replace(EXTENSION_IDENTIFIER_REGEX, (match, publisher: string, name: string) => getGalleryExtensionId(publisher, name)); -} - export function getLocalExtensionId(id: string, version: string): string { return `${id}-${version}`; } @@ -117,4 +119,27 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set(); + +export function isWorkspaceExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const configuredWorkspaceExtensions = configurationService.getValue('_workbench.workspaceExtensions') || []; + if (configuredWorkspaceExtensions.length) { + if (configuredWorkspaceExtensions.indexOf(extensionId) !== -1) { + return true; + } + if (configuredWorkspaceExtensions.indexOf(`-${extensionId}`) !== -1) { + return false; + } + } + + if (manifest.main) { + if ((manifest.categories || []).indexOf('Workspace Extension') !== -1) { + return true; + } + return !nonWorkspaceExtensions.has(extensionId); + } + return false; } \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index cec901d0b77..3d642baf387 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { tmpdir } from 'os'; import * as path from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; import { distinct } from 'vs/base/common/arrays'; -import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; +import { getErrorMessage, isPromiseCanceledError, canceled } from 'vs/base/common/errors'; import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier, IReportedExtension, InstallOperation, ITranslation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { assign, getOrDefault } from 'vs/base/common/objects'; @@ -24,6 +23,7 @@ import { readFile } from 'vs/base/node/pfs'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; import { generateUuid, isUUID } from 'vs/base/common/uuid'; import { values } from 'vs/base/common/map'; +import { CancellationToken } from 'vs/base/common/cancellation'; interface IRawGalleryExtensionFile { assetType: string; @@ -115,6 +115,7 @@ const AssetType = { const PropertyType = { Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies', + ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine' }; @@ -262,8 +263,8 @@ function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IG }; } -function getDependencies(version: IRawGalleryExtensionVersion): string[] { - const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Dependency) : []; +function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] { + const values = version.properties ? version.properties.filter(p => p.key === property) : []; const value = values.length > 0 && values[0].value; return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : []; } @@ -277,8 +278,7 @@ function getIsPreview(flags: string): boolean { return flags.indexOf('preview') !== -1; } -function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUrl: string, index: number, query: Query, querySource?: string): IGalleryExtension { - const [version] = galleryExtension.versions; +function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension { const assets = { manifest: getVersionAsset(version, AssetType.Manifest), readme: getVersionAsset(version, AssetType.Details), @@ -303,12 +303,13 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr publisher: galleryExtension.publisher.publisherName, publisherDisplayName: galleryExtension.publisher.displayName, description: galleryExtension.shortDescription || '', - installCount: getStatistic(galleryExtension.statistics, 'install'), + installCount: getStatistic(galleryExtension.statistics, 'install') + getStatistic(galleryExtension.statistics, 'updateCount'), rating: getStatistic(galleryExtension.statistics, 'averagerating'), ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'), assets, properties: { - dependencies: getDependencies(version), + dependencies: getExtensions(version, PropertyType.Dependency), + extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version) }, /* __GDPR__FRAGMENT__ @@ -360,6 +361,31 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } + getExtension({ id, uuid }: IExtensionIdentifier, version?: string): TPromise { + let query = new Query() + .withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) + .withPage(1, 1) + .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') + .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + + if (uuid) { + query = query.withFilter(FilterType.ExtensionId, uuid); + } else { + 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 null; + }); + } + query(options: IQueryOptions = {}): TPromise> { if (!this.isEnabled()) { return TPromise.wrapError>(new Error('No extension gallery service configured.')); @@ -420,20 +446,24 @@ export class ExtensionGalleryService implements IExtensionGalleryService { query = query.withSortOrder(options.sortOrder); } - return this.queryGallery(query).then(({ galleryExtensions, total }) => { - const extensions = galleryExtensions.map((e, index) => toExtension(e, this.extensionsGalleryUrl, index, query, options.source)); + return this.queryGallery(query, CancellationToken.None).then(({ galleryExtensions, total }) => { + const extensions = galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, query, options.source)); const pageSize = query.pageSize; - const getPage = (pageIndex: number) => { + const getPage = (pageIndex: number, ct: CancellationToken) => { + if (ct.isCancellationRequested) { + return TPromise.wrapError(canceled()); + } + const nextPageQuery = query.withPage(pageIndex + 1); - return this.queryGallery(nextPageQuery) - .then(({ galleryExtensions }) => galleryExtensions.map((e, index) => toExtension(e, this.extensionsGalleryUrl, index, nextPageQuery, options.source))); + return this.queryGallery(nextPageQuery, ct) + .then(({ galleryExtensions }) => galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, nextPageQuery, options.source))); }; - return { firstPage: extensions, total, pageSize, getPage }; + return { firstPage: extensions, total, pageSize, getPage } as IPager; }); } - private queryGallery(query: Query): TPromise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { + private queryGallery(query: Query, token: CancellationToken): TPromise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { return this.commonHeadersPromise.then(commonHeaders => { const data = JSON.stringify(query.raw); const headers = assign({}, commonHeaders, { @@ -448,7 +478,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { url: this.api('/extensionquery'), data, headers - }).then(context => { + }, token).then(context => { if (context.res.statusCode >= 400 && context.res.statusCode < 500) { return { galleryExtensions: [], total: 0 }; @@ -478,49 +508,43 @@ export class ExtensionGalleryService implements IExtensionGalleryService { type: 'POST', url: this.api(`/publishers/${publisher}/extensions/${name}/${version}/stats?statType=${type}`), headers - }).then(null, () => null); + }, CancellationToken.None).then(null, () => null); }); } download(extension: IGalleryExtension, operation: InstallOperation): TPromise { - return this.loadCompatibleVersion(extension) - .then(extension => { - if (!extension) { - return TPromise.wrapError(new Error(localize('notCompatibleDownload', "Unable to download because the extension compatible with current version '{0}' of VS Code is not found.", pkg.version))); - } - const zipPath = path.join(tmpdir(), generateUuid()); - const data = getGalleryExtensionTelemetryData(extension); - const startTime = new Date().getTime(); - /* __GDPR__ - "galleryService:downloadVSIX" : { - "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', assign(data, { duration })); + const zipPath = path.join(tmpdir(), generateUuid()); + const data = getGalleryExtensionTelemetryData(extension); + const startTime = new Date().getTime(); + /* __GDPR__ + "galleryService:downloadVSIX" : { + "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', assign(data, { duration })); - const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; - const downloadAsset = operationParam ? { - uri: `${extension.assets.download.uri}&${operationParam}=true`, - fallbackUri: `${extension.assets.download.fallbackUri}?${operationParam}=true` - } : extension.assets.download; + const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; + const downloadAsset = operationParam ? { + uri: `${extension.assets.download.uri}&${operationParam}=true`, + fallbackUri: `${extension.assets.download.fallbackUri}?${operationParam}=true` + } : extension.assets.download; - return this.getAsset(downloadAsset) - .then(context => download(zipPath, context)) - .then(() => log(new Date().getTime() - startTime)) - .then(() => zipPath); - }); + return this.getAsset(downloadAsset) + .then(context => download(zipPath, context)) + .then(() => log(new Date().getTime() - startTime)) + .then(() => zipPath); } - getReadme(extension: IGalleryExtension): TPromise { - return this.getAsset(extension.assets.readme) + getReadme(extension: IGalleryExtension, token: CancellationToken): TPromise { + return this.getAsset(extension.assets.readme, {}, token) .then(asText); } - getManifest(extension: IGalleryExtension): TPromise { - return this.getAsset(extension.assets.manifest) + getManifest(extension: IGalleryExtension, token: CancellationToken): TPromise { + return this.getAsset(extension.assets.manifest, {}, token) .then(asText) .then(JSON.parse); } @@ -535,13 +559,13 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return TPromise.as(null); } - getChangelog(extension: IGalleryExtension): TPromise { - return this.getAsset(extension.assets.changelog) + getChangelog(extension: IGalleryExtension, token: CancellationToken): TPromise { + return this.getAsset(extension.assets.changelog, {}, token) .then(asText); } - loadAllDependencies(extensions: IExtensionIdentifier[]): TPromise { - return this.getDependenciesReccursively(extensions.map(e => e.id), []); + loadAllDependencies(extensions: IExtensionIdentifier[], token: CancellationToken): TPromise { + return this.getDependenciesReccursively(extensions.map(e => e.id), [], token); } loadCompatibleVersion(extension: IGalleryExtension): TPromise { @@ -557,7 +581,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { .withAssetTypes(AssetType.Manifest, AssetType.VSIX) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - return this.queryGallery(query) + return this.queryGallery(query, CancellationToken.None) .then(({ galleryExtensions }) => { const [rawExtension] = galleryExtensions; @@ -568,7 +592,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.getLastValidExtensionVersion(rawExtension, rawExtension.versions) .then(rawVersion => { if (rawVersion) { - extension.properties.dependencies = getDependencies(rawVersion); + extension.properties.dependencies = getExtensions(rawVersion, PropertyType.Dependency); extension.properties.engine = getEngine(rawVersion); extension.assets.download = getVersionAsset(rawVersion, AssetType.VSIX); extension.version = rawVersion.version; @@ -579,7 +603,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - private loadDependencies(extensionNames: string[]): TPromise { + private loadDependencies(extensionNames: string[], token: CancellationToken): TPromise { if (!extensionNames || extensionNames.length === 0) { return TPromise.as([]); } @@ -592,14 +616,14 @@ export class ExtensionGalleryService implements IExtensionGalleryService { .withAssetTypes(AssetType.Icon, AssetType.License, AssetType.Details, AssetType.Manifest, AssetType.VSIX) .withFilter(FilterType.ExtensionName, ...extensionNames); - return this.queryGallery(query).then(result => { + return this.queryGallery(query, token).then(result => { const dependencies = []; const ids = []; for (let index = 0; index < result.galleryExtensions.length; index++) { const rawExtension = result.galleryExtensions[index]; if (ids.indexOf(rawExtension.extensionId) === -1) { - dependencies.push(toExtension(rawExtension, this.extensionsGalleryUrl, index, query, 'dependencies')); + dependencies.push(toExtension(rawExtension, rawExtension.versions[0], index, query, 'dependencies')); ids.push(rawExtension.extensionId); } } @@ -607,7 +631,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - private getDependenciesReccursively(toGet: string[], result: IGalleryExtension[]): TPromise { + private getDependenciesReccursively(toGet: string[], result: IGalleryExtension[], token: CancellationToken): TPromise { if (!toGet || !toGet.length) { return TPromise.wrap(result); } @@ -616,7 +640,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return TPromise.wrap(result); } - return this.loadDependencies(toGet) + return this.loadDependencies(toGet, token) .then(loadedDependencies => { const dependenciesSet = new Set(); for (const dep of loadedDependencies) { @@ -627,11 +651,11 @@ export class ExtensionGalleryService implements IExtensionGalleryService { result = distinct(result.concat(loadedDependencies), d => d.identifier.uuid); const dependencies: string[] = []; dependenciesSet.forEach(d => !ExtensionGalleryService.hasExtensionByName(result, d) && dependencies.push(d)); - return this.getDependenciesReccursively(dependencies, result); + return this.getDependenciesReccursively(dependencies, result, token); }); } - private getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}): TPromise { + private getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): TPromise { return this.commonHeadersPromise.then(commonHeaders => { const baseOptions = { type: 'GET' }; const headers = assign({}, commonHeaders, options.headers || {}); @@ -641,7 +665,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const fallbackUrl = asset.fallbackUri; const firstOptions = assign({}, options, { url }); - return this.requestService.request(firstOptions) + return this.requestService.request(firstOptions, token) .then(context => { if (context.res.statusCode === 200) { return TPromise.as(context); @@ -673,7 +697,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { this.telemetryService.publicLog('galleryService:cdnFallback', { url, message }); const fallbackOptions = assign({}, options, { url: fallbackUrl }); - return this.requestService.request(fallbackOptions).then(null, err => { + return this.requestService.request(fallbackOptions, token).then(null, err => { if (isPromiseCanceledError(err)) { return TPromise.wrapError(err); } @@ -756,7 +780,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return TPromise.as([]); } - return this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }).then(context => { + return this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }, CancellationToken.None).then(context => { if (context.res.statusCode !== 200) { return TPromise.wrapError(new Error('Could not get extensions report.')); } @@ -804,4 +828,4 @@ export function resolveMarketplaceHeaders(environmentService: IEnvironmentServic 'X-Market-User-Id': uuid }; }); -} \ No newline at end of file +} diff --git a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts index cbeef87b2bc..7589d439876 100644 --- a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts +++ b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts @@ -10,9 +10,10 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ILogService } from 'vs/platform/log/common/log'; import { fork, ChildProcess } from 'child_process'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { join } from 'vs/base/common/paths'; +import { posix } from 'path'; import { Limiter } from 'vs/base/common/async'; import { fromNodeEventEmitter, anyEvent, mapEvent, debounceEvent } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; export class ExtensionsLifecycle extends Disposable { @@ -36,13 +37,13 @@ export class ExtensionsLifecycle extends Disposable { } private parseUninstallScript(extension: ILocalExtension): { uninstallHook: string, args: string[] } { - if (extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts']['vscode:uninstall'] === 'string') { + if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts']['vscode:uninstall'] === 'string') { const uninstallScript = (extension.manifest['scripts']['vscode:uninstall']).split(' '); if (uninstallScript.length < 2 || uninstallScript[0] !== 'node' || !uninstallScript[1]) { this.logService.warn(extension.identifier.id, 'Uninstall script should be a node script'); return null; } - return { uninstallHook: join(extension.path, uninstallScript[1]), args: uninstallScript.slice(2) || [] }; + return { uninstallHook: posix.join(extension.location.fsPath, uninstallScript[1]), args: uninstallScript.slice(2) || [] }; } return null; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts new file mode 100644 index 00000000000..5c20a9e30b0 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionManagementIpc.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, LocalExtensionType, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from '../common/extensionManagement'; +import { Event, buffer, mapEvent } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { IURITransformer } from 'vs/base/common/uriIpc'; + +export interface IExtensionManagementChannel extends IChannel { + listen(event: 'onInstallExtension'): Event; + listen(event: 'onDidInstallExtension'): Event; + listen(event: 'onUninstallExtension'): Event; + listen(event: 'onDidUninstallExtension'): Event; + + call(command: 'zip', args: [ILocalExtension]): Thenable; + call(command: 'unzip', args: [URI, LocalExtensionType]): Thenable; + call(command: 'install', args: [URI]): Thenable; + call(command: 'installFromGallery', args: [IGalleryExtension]): Thenable; + call(command: 'uninstall', args: [ILocalExtension, boolean]): Thenable; + call(command: 'reinstallFromGallery', args: [ILocalExtension]): Thenable; + call(command: 'getInstalled', args: [LocalExtensionType]): Thenable; + call(command: 'getExtensionsReport'): Thenable; + call(command: 'updateMetadata', args: [ILocalExtension, IGalleryMetadata]): Thenable; +} + +export class ExtensionManagementChannel implements IExtensionManagementChannel { + + onInstallExtension: Event; + onDidInstallExtension: Event; + onUninstallExtension: Event; + onDidUninstallExtension: Event; + + constructor(private service: IExtensionManagementService) { + this.onInstallExtension = buffer(service.onInstallExtension, true); + this.onDidInstallExtension = buffer(service.onDidInstallExtension, true); + this.onUninstallExtension = buffer(service.onUninstallExtension, true); + this.onDidUninstallExtension = buffer(service.onDidUninstallExtension, true); + } + + listen(event: string): Event { + switch (event) { + case 'onInstallExtension': return this.onInstallExtension; + case 'onDidInstallExtension': return this.onDidInstallExtension; + case 'onUninstallExtension': return this.onUninstallExtension; + case 'onDidUninstallExtension': return this.onDidUninstallExtension; + } + + throw new Error('Invalid listen'); + } + + call(command: string, args?: any): Thenable { + switch (command) { + case 'zip': return this.service.zip(this._transform(args[0])); + case 'unzip': return this.service.unzip(URI.revive(args[0]), args[1]); + case 'install': return this.service.install(URI.revive(args[0])); + case 'installFromGallery': return this.service.installFromGallery(args[0]); + case 'uninstall': return this.service.uninstall(this._transform(args[0]), args[1]); + case 'reinstallFromGallery': return this.service.reinstallFromGallery(this._transform(args[0])); + case 'getInstalled': return this.service.getInstalled(args[0]); + case 'updateMetadata': return this.service.updateMetadata(this._transform(args[0]), args[1]); + case 'getExtensionsReport': return this.service.getExtensionsReport(); + } + + throw new Error('Invalid call'); + } + + private _transform(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: URI.revive(extension.location) } } : extension; + } +} + +export class ExtensionManagementChannelClient implements IExtensionManagementService { + + _serviceBrand: any; + + constructor(private channel: IExtensionManagementChannel, private uriTransformer: IURITransformer) { } + + get onInstallExtension(): Event { return this.channel.listen('onInstallExtension'); } + get onDidInstallExtension(): Event { return mapEvent(this.channel.listen('onDidInstallExtension'), i => ({ ...i, local: this._transformIncoming(i.local) })); } + get onUninstallExtension(): Event { return this.channel.listen('onUninstallExtension'); } + get onDidUninstallExtension(): Event { return this.channel.listen('onDidUninstallExtension'); } + + zip(extension: ILocalExtension): TPromise { + return TPromise.wrap(this.channel.call('zip', [this._transformOutgoing(extension)]).then(result => URI.revive(this.uriTransformer.transformIncoming(result)))); + } + + unzip(zipLocation: URI, type: LocalExtensionType): TPromise { + return TPromise.wrap(this.channel.call('unzip', [this.uriTransformer.transformOutgoing(zipLocation), type])); + } + + install(vsix: URI): TPromise { + return TPromise.wrap(this.channel.call('install', [this.uriTransformer.transformOutgoing(vsix)])); + } + + installFromGallery(extension: IGalleryExtension): TPromise { + return TPromise.wrap(this.channel.call('installFromGallery', [extension])); + } + + uninstall(extension: ILocalExtension, force = false): TPromise { + return TPromise.wrap(this.channel.call('uninstall', [this._transformOutgoing(extension), force])); + } + + reinstallFromGallery(extension: ILocalExtension): TPromise { + return TPromise.wrap(this.channel.call('reinstallFromGallery', [this._transformOutgoing(extension)])); + } + + getInstalled(type: LocalExtensionType = null): TPromise { + return TPromise.wrap(this.channel.call('getInstalled', [type])) + .then(extensions => extensions.map(extension => this._transformIncoming(extension))); + } + + updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise { + return TPromise.wrap(this.channel.call('updateMetadata', [this._transformOutgoing(local), metadata])) + .then(extension => this._transformIncoming(extension)); + } + + getExtensionsReport(): TPromise { + return TPromise.wrap(this.channel.call('getExtensionsReport')); + } + + private _transformIncoming(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: URI.revive(this.uriTransformer.transformIncoming(extension.location)) } } : extension; + } + + private _transformOutgoing(extension: ILocalExtension): ILocalExtension { + return extension ? { ...extension, ...{ location: this.uriTransformer.transformOutgoing(extension.location) } } : extension; + } + +} \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 5d7e31e087c..d11c7424985 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -11,9 +11,9 @@ import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { flatten, distinct } from 'vs/base/common/arrays'; -import { extract, buffer, ExtractError } from 'vs/base/node/zip'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { flatten } from 'vs/base/common/arrays'; +import { extract, buffer, ExtractError, zip, IFile } from 'vs/platform/node/zip'; +import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IExtensionManifest, IGalleryMetadata, @@ -26,10 +26,10 @@ import { import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from '../common/extensionNls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Limiter, always } from 'vs/base/common/async'; +import { Limiter, always, createCancelablePromise, CancelablePromise } 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'; +import { URI } from 'vs/base/common/uri'; import pkg from 'vs/platform/node/package'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -40,8 +40,13 @@ import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extens import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; +import { tmpdir } from 'os'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { Schemas } from 'vs/base/common/network'; +import { CancellationToken } from 'vs/base/common/cancellation'; -const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser'; const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled'; @@ -53,6 +58,7 @@ const INSTALL_ERROR_LOCAL = 'local'; const INSTALL_ERROR_EXTRACTING = 'extracting'; const INSTALL_ERROR_RENAMING = 'renaming'; const INSTALL_ERROR_DELETING = 'deleting'; +const INSTALL_ERROR_MALICIOUS = 'malicious'; const ERROR_UNKNOWN = 'unknown'; export class ExtensionManagementError extends Error { @@ -107,13 +113,14 @@ export class ExtensionManagementService extends Disposable implements IExtension _serviceBrand: any; + private systemExtensionsPath: string; private extensionsPath: string; private uninstalledPath: string; private uninstalledFileLimiter: Limiter; private reportedExtensions: TPromise | undefined; private lastReportTimestamp = 0; - private readonly installationStartTime: Map = new Map(); - private readonly installingExtensions: Map> = new Map>(); + private readonly installingExtensions: Map> = new Map>(); + private readonly uninstallingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionLifecycle: ExtensionsLifecycle; @@ -134,48 +141,108 @@ export class ExtensionManagementService extends Disposable implements IExtension @IDialogService private dialogService: IDialogService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, @ILogService private logService: ILogService, + @optional(IDownloadService) private downloadService: IDownloadService, @ITelemetryService private telemetryService: ITelemetryService, ) { super(); + this.systemExtensionsPath = environmentService.builtinExtensionsPath; this.extensionsPath = environmentService.extensionsPath; this.uninstalledPath = path.join(this.extensionsPath, '.obsolete'); this.uninstalledFileLimiter = new Limiter(1); - this._register(toDisposable(() => this.installingExtensions.clear())); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionLifecycle = this._register(new ExtensionsLifecycle(this.logService)); + + this._register(toDisposable(() => { + this.installingExtensions.forEach(promise => promise.cancel()); + this.uninstallingExtensions.forEach(promise => promise.cancel()); + this.installingExtensions.clear(); + this.uninstallingExtensions.clear(); + })); } - install(zipPath: string): TPromise { - zipPath = path.resolve(zipPath); + zip(extension: ILocalExtension): TPromise { + return TPromise.wrap(this.collectFiles(extension)) + .then(files => zip(path.join(tmpdir(), generateUuid()), files)) + .then(path => URI.file(path)); + } - return validateLocalExtension(zipPath) - .then(manifest => { - const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; - if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) { - return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); + unzip(zipLocation: URI, type: LocalExtensionType): TPromise { + return this.install(zipLocation, type); + } + + private collectFiles(extension: ILocalExtension): Promise { + + const collectFilesFromDirectory = async (dir): Promise => { + let entries = await pfs.readdir(dir); + entries = entries.map(e => path.join(dir, e)); + const stats = await Promise.all(entries.map(e => pfs.stat(e))); + let promise: Promise = Promise.resolve([]); + stats.forEach((stat, index) => { + const entry = entries[index]; + if (stat.isFile()) { + promise = promise.then(result => ([...result, entry])); + } + if (stat.isDirectory()) { + promise = promise + .then(result => collectFilesFromDirectory(entry) + .then(files => ([...result, ...files]))); } - return this.removeIfExists(identifier.id) - .then( - () => this.checkOutdated(manifest) - .then(validated => { - if (validated) { - this.logService.info('Installing the extension:', identifier.id); - this._onInstallExtension.fire({ identifier, zipPath }); - return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) - .then( - metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest), - error => this.installFromZipPath(identifier, zipPath, null, manifest)) - .then( - local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, - e => { - this.logService.error('Failed to install the extension:', identifier.id, e.message); - return TPromise.wrapError(e); - }); - } - return null; - }), - e => TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)))); }); + return promise; + }; + + return collectFilesFromDirectory(extension.location.fsPath) + .then(files => files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }))); + + } + + install(vsix: URI, type: LocalExtensionType = LocalExtensionType.User): TPromise { + return TPromise.wrap(createCancelablePromise(token => { + return this.downloadVsix(vsix) + .then(downloadLocation => { + const zipPath = path.resolve(downloadLocation.fsPath); + + return validateLocalExtension(zipPath) + .then(manifest => { + const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) { + return TPromise.wrapError(new Error(nls.localize('incompatible', "Unable to install Extension '{0}' as it is not compatible with Code '{1}'.", identifier.id, pkg.version))); + } + return this.removeIfExists(identifier.id) + .then( + () => this.checkOutdated(manifest) + .then(validated => { + if (validated) { + this.logService.info('Installing the extension:', identifier.id); + this._onInstallExtension.fire({ identifier, zipPath }); + return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) + .then( + metadata => this.installFromZipPath(identifier, zipPath, metadata, type, token), + error => this.installFromZipPath(identifier, zipPath, null, type, token)) + .then( + () => { this.logService.info('Successfully installed the extension:', identifier.id); return identifier; }, + e => { + this.logService.error('Failed to install the extension:', identifier.id, e.message); + return TPromise.wrapError(e); + }); + } + return null; + }), + e => TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)))); + }); + }); + })); + } + + private downloadVsix(vsix: URI): TPromise { + if (vsix.scheme === Schemas.file) { + return TPromise.as(vsix); + } + if (!this.downloadService) { + throw new Error('Download service is not available'); + } + const downloadedLocation = path.join(tmpdir(), generateUuid()); + return this.downloadService.download(vsix, downloadedLocation).then(() => URI.file(downloadedLocation)); } private removeIfExists(id: string): TPromise { @@ -207,52 +274,74 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise { - return this.getInstalled() + private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, type: LocalExtensionType, token: CancellationToken): TPromise { + return this.toNonCancellablePromise(this.getInstalled() .then(installed => { const operation = this.getOperation({ id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid }, installed); - return this.installExtension({ zipPath, id: identifier.id, metadata }) - .then(local => { - if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) { - return this.getDependenciesToInstall(local.manifest.extensionDependencies) - .then(dependenciesToInstall => { - dependenciesToInstall = metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall; - return this.downloadAndInstallExtensions(dependenciesToInstall, dependenciesToInstall.map(d => this.getOperation(d.identifier, installed))); - }) - .then(() => local, error => { - this.setUninstalled(local); - return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error))); - }); - } - return local; - }) + return this.installExtension({ zipPath, id: identifier.id, metadata }, type, token) + .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))) .then( local => { this._onDidInstallExtension.fire({ identifier, zipPath, local, operation }); return local; }, error => { this._onDidInstallExtension.fire({ identifier, zipPath, operation, error }); return TPromise.wrapError(error); } ); - }); + })); } - installFromGallery(extension: IGalleryExtension): TPromise { - this.onInstallExtensions([extension]); - return this.getInstalled(LocalExtensionType.User) - .then(installed => this.collectExtensionsToInstall(extension) - .then( - extensionsToInstall => { - if (extensionsToInstall.length > 1) { - this.onInstallExtensions(extensionsToInstall.slice(1)); - } - const operataions: InstallOperation[] = extensionsToInstall.map(e => this.getOperation(e.identifier, installed)); - return this.downloadAndInstallExtensions(extensionsToInstall, operataions) - .then( - locals => this.onDidInstallExtensions(extensionsToInstall, locals, operataions, []) - .then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier))[0]), - errors => this.onDidInstallExtensions(extensionsToInstall, [], operataions, errors)); - }, - error => this.onDidInstallExtensions([extension], [], [this.getOperation(extension.identifier, installed)], [error]))); + installFromGallery(extension: IGalleryExtension): TPromise { + let cancellablePromise = this.installingExtensions.get(extension.identifier.id); + if (!cancellablePromise) { + + let cancellationToken: CancellationToken, successCallback: ValueCallback, errorCallback: ErrorCallback; + cancellablePromise = createCancelablePromise(token => { cancellationToken = token; return new TPromise((c, e) => { successCallback = c; errorCallback = e; }); }); + this.installingExtensions.set(extension.identifier.id, cancellablePromise); + + try { + const startTime = new Date().getTime(); + const identifier = { id: getLocalExtensionIdFromGallery(extension, extension.version), uuid: extension.identifier.uuid }; + const telemetryData = getGalleryExtensionTelemetryData(extension); + let operation: InstallOperation = InstallOperation.Install; + + this.logService.info('Installing extension:', extension.name); + this._onInstallExtension.fire({ identifier, gallery: extension }); + + this.checkMalicious(extension) + .then(() => this.getInstalled(LocalExtensionType.User)) + .then(installed => { + const existingExtension = installed.filter(i => areSameExtensions(i.galleryIdentifier, extension.identifier))[0]; + operation = existingExtension ? InstallOperation.Update : InstallOperation.Install; + return this.downloadInstallableExtension(extension, operation) + .then(installableExtension => this.installExtension(installableExtension, LocalExtensionType.User, cancellationToken).then(local => always(pfs.rimraf(installableExtension.zipPath), () => null).then(() => local))) + .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) + .then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error)))); + }) + .then( + local => { + this.installingExtensions.delete(extension.identifier.id); + this.logService.info(`Extensions installed successfully:`, extension.identifier.id); + this._onDidInstallExtension.fire({ identifier, gallery: extension, local, operation }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, void 0); + successCallback(null); + }, + error => { + this.installingExtensions.delete(extension.identifier.id); + const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; + this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode); + this._onDidInstallExtension.fire({ identifier, gallery: extension, operation, error: errorCode }); + this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, error); + errorCallback(error); + }); + + } catch (error) { + this.installingExtensions.delete(extension.identifier.id); + errorCallback(error); + } + + } + + return TPromise.wrap(cancellablePromise); } - reinstallFromGallery(extension: ILocalExtension): TPromise { + reinstallFromGallery(extension: ILocalExtension): TPromise { if (!this.galleryService.isEnabled()) { return TPromise.wrapError(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); } @@ -273,46 +362,19 @@ export class ExtensionManagementService extends Disposable implements IExtension return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall)) ? InstallOperation.Update : InstallOperation.Install; } - private collectExtensionsToInstall(extension: IGalleryExtension): TPromise { - return this.galleryService.loadCompatibleVersion(extension) - .then(compatible => { - if (!compatible) { - return TPromise.wrapError(new ExtensionManagementError(nls.localize('notFoundCompatible', "Unable to install '{0}'; there is no available version compatible with VS Code '{1}'.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); + private getTelemetryEvent(operation: InstallOperation): string { + return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install'; + } + + private checkMalicious(extension: IGalleryExtension): TPromise { + return this.getExtensionsReport() + .then(report => { + if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { + throw new ExtensionManagementError(INSTALL_ERROR_MALICIOUS, nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); + } else { + return null; } - return this.getDependenciesToInstall(compatible.properties.dependencies) - .then( - dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]), - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); - } - - private downloadAndInstallExtensions(extensions: IGalleryExtension[], operations: InstallOperation[]): TPromise { - return TPromise.join(extensions.map((extensionToInstall, index) => this.downloadAndInstallExtension(extensionToInstall, operations[index]))) - .then(null, errors => this.rollback(extensions).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); - } - - private downloadAndInstallExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { - let installingExtension = this.installingExtensions.get(extension.identifier.id); - if (!installingExtension) { - installingExtension = this.getExtensionsReport() - .then(report => { - if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) { - throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic.")); - } else { - return extension; - } - }) - .then(extension => this.downloadInstallableExtension(extension, operation)) - .then(installableExtension => this.installExtension(installableExtension)) - .then( - local => { this.installingExtensions.delete(extension.identifier.id); return local; }, - e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); } - ); - - this.installingExtensions.set(extension.identifier.id, installingExtension); - } - return installingExtension; + }); } private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise { @@ -327,10 +389,10 @@ export class ExtensionManagementService extends Disposable implements IExtension compatible => { if (compatible) { this.logService.trace('Started downloading extension:', extension.name); - return this.galleryService.download(extension, operation) + return this.galleryService.download(compatible, operation) .then( zipPath => { - this.logService.info('Downloaded extension:', extension.name); + this.logService.info('Downloaded extension:', extension.name, zipPath); return validateLocalExtension(zipPath) .then( manifest => ({ zipPath, id: getLocalExtensionIdFromManifest(manifest), metadata }), @@ -345,62 +407,14 @@ export class ExtensionManagementService extends Disposable implements IExtension error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); } - private onInstallExtensions(extensions: IGalleryExtension[]): void { - for (const extension of extensions) { - this.logService.info('Installing extension:', extension.name); - this.installationStartTime.set(extension.identifier.id, new Date().getTime()); - const id = getLocalExtensionIdFromGallery(extension, extension.version); - this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension }); - } - } - - private onDidInstallExtensions(extensions: IGalleryExtension[], locals: ILocalExtension[], operations: InstallOperation[], errors: Error[]): TPromise { - extensions.forEach((gallery, index) => { - const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid }; - const local = locals[index]; - const error = errors[index]; - const operation = operations[index]; - if (local) { - this.logService.info(`Extensions installed successfully:`, gallery.identifier.id); - this._onDidInstallExtension.fire({ identifier, gallery, local, operation }); - } else { - const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; - this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode); - this._onDidInstallExtension.fire({ identifier, gallery, operation, error: errorCode }); - } - const startTime = this.installationStartTime.get(gallery.identifier.id); - this.reportTelemetry(operations[index] === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error); - this.installationStartTime.delete(gallery.identifier.id); - }); - return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null); - } - - private getDependenciesToInstall(dependencies: string[]): TPromise { - if (dependencies.length) { - return this.getInstalled() - .then(installed => { - const uninstalledDeps = dependencies.filter(d => installed.every(i => getGalleryExtensionId(i.manifest.publisher, i.manifest.name) !== d)); - if (uninstalledDeps.length) { - return this.galleryService.loadAllDependencies(uninstalledDeps.map(id => ({ id }))) - .then(allDependencies => allDependencies.filter(d => { - const extensionId = getLocalExtensionIdFromGallery(d, d.version); - return installed.every(({ identifier }) => identifier.id !== extensionId); - })); - } - return []; - }); - } - return TPromise.as([]); - } - - private installExtension(installableExtension: InstallableExtension): TPromise { + private installExtension(installableExtension: InstallableExtension, type: LocalExtensionType, token: CancellationToken): TPromise { return this.unsetUninstalledAndGetLocal(installableExtension.id) .then( local => { if (local) { return local; } - return this.extractAndInstall(installableExtension); + return this.extractAndInstall(installableExtension, type, token); }, e => { if (isMacintosh) { @@ -427,13 +441,15 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private extractAndInstall({ zipPath, id, metadata }: InstallableExtension): TPromise { - const tempPath = path.join(this.extensionsPath, `.${id}`); - const extensionPath = path.join(this.extensionsPath, id); - return this.extractAndRename(id, zipPath, tempPath, extensionPath) + private extractAndInstall({ zipPath, id, metadata }: InstallableExtension, type: LocalExtensionType, token: CancellationToken): TPromise { + const location = type === LocalExtensionType.User ? this.extensionsPath : this.systemExtensionsPath; + const tempPath = path.join(location, `.${id}`); + const extensionPath = path.join(location, id); + return pfs.rimraf(extensionPath) + .then(() => this.extractAndRename(id, zipPath, tempPath, extensionPath, token), e => TPromise.wrapError(new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, id), INSTALL_ERROR_DELETING))) .then(() => { this.logService.info('Installation completed.', id); - return this.scanExtension(id, this.extensionsPath, LocalExtensionType.User); + return this.scanExtension(id, location, type); }) .then(local => { if (metadata) { @@ -444,9 +460,9 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private extractAndRename(id: string, zipPath: string, extractPath: string, renamePath: string): TPromise { - return this.extract(id, zipPath, extractPath) - .then(() => this.rename(id, extractPath, renamePath, Date.now() + (30 * 1000) /* Retry for 30 seconds */) + private extractAndRename(id: string, zipPath: string, extractPath: string, renamePath: string, token: CancellationToken): TPromise { + return this.extract(id, zipPath, extractPath, token) + .then(() => this.rename(id, extractPath, renamePath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */) .then( () => this.logService.info('Renamed to', renamePath), e => { @@ -455,11 +471,11 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - private extract(id: string, zipPath: string, extractPath: string): TPromise { + private extract(id: string, zipPath: string, extractPath: string, token: CancellationToken): TPromise { this.logService.trace(`Started extracting the extension from ${zipPath} to ${extractPath}`); return pfs.rimraf(extractPath) .then( - () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, this.logService) + () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, this.logService, token) .then( () => this.logService.info(`Extracted extension to ${extractPath}:`, id), e => always(pfs.rimraf(extractPath), () => null) @@ -478,22 +494,55 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } + private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension): TPromise { + if (this.galleryService.isEnabled()) { + const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; + if (installed.manifest.extensionPack) { + for (const extension of installed.manifest.extensionPack) { + // add only those extensions which are new in currently installed extension + if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) { + if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) { + dependenciesAndPackExtensions.push(extension); + } + } + } + } + if (dependenciesAndPackExtensions.length) { + return this.getInstalled() + .then(installed => { + // filter out installing and installed extensions + const names = dependenciesAndPackExtensions.filter(id => !this.installingExtensions.has(adoptToGalleryExtensionId(id)) && installed.every(({ galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id }))); + if (names.length) { + return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }) + .then(galleryResult => { + const extensionsToInstall = galleryResult.firstPage; + return TPromise.join(extensionsToInstall.map(e => this.installFromGallery(e))) + .then(() => null, errors => this.rollback(extensionsToInstall).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors))); + }); + } + return null; + }); + } + } + return TPromise.as(null); + } + private rollback(extensions: IGalleryExtension[]): TPromise { return this.getInstalled(LocalExtensionType.User) .then(installed => TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version - .map(local => this.setUninstalled(local)))) + .map(local => this.uninstall(local, true)))) .then(() => null, () => null); } uninstall(extension: ILocalExtension, force = false): TPromise { - return this.getInstalled(LocalExtensionType.User) + return this.toNonCancellablePromise(this.getInstalled(LocalExtensionType.User) .then(installed => { const promises = installed .filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name) - .map(e => this.checkForDependenciesAndUninstall(e, installed, force)); + .map(e => this.checkForDependenciesAndUninstall(e, installed)); return TPromise.join(promises).then(() => null, error => TPromise.wrapError(this.joinErrors(error))); - }); + })); } updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise { @@ -548,9 +597,15 @@ export class ExtensionManagementService extends Disposable implements IExtension }, new Error('')); } - private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { + private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise { return this.preUninstallExtension(extension) - .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.uninstallWithDependencies(extension, [], installed)) + .then(() => { + const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); + if (packedExtensions.length) { + return this.uninstallExtensions(extension, packedExtensions, installed); + } + return this.uninstallExtensions(extension, [], installed); + }) .then(() => this.postUninstallExtension(extension), error => { this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); @@ -558,47 +613,15 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean { - if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) { - return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); - } - return false; - } - - private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise { - if (force) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); - } - - const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name); - const buttons = [ - nls.localize('uninstallOnly', "Extension Only"), - nls.localize('uninstallAll', "Uninstall All"), - nls.localize('cancel', "Cancel") - ]; - this.logService.info('Requesting for confirmation to uninstall extension with dependencies', extension.identifier.id); - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) - .then(value => { - if (value === 0) { - return this.uninstallWithDependencies(extension, [], installed); - } - if (value === 1) { - const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension); - return this.uninstallWithDependencies(extension, dependencies, installed); - } - this.logService.info('Cancelled uninstalling extension:', extension.identifier.id); - return TPromise.wrapError(errors.canceled()); - }, error => TPromise.wrapError(errors.canceled())); - } - - private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise { - const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed); - let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1); + private uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): TPromise { + const dependents = this.getDependents(extension, installed); if (dependents.length) { - return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, dependents))); + const remainingDependents = dependents.filter(dependent => extension !== dependent && otherExtensionsToUninstall.indexOf(dependent) === -1); + if (remainingDependents.length) { + return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, remainingDependents))); + } } - return TPromise.join([this.uninstallExtension(extension), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null); + return TPromise.join([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]).then(() => null); } private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string { @@ -614,37 +637,24 @@ export class ExtensionManagementService extends Disposable implements IExtension extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name); } - private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] { + private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] { if (checked.indexOf(extension) !== -1) { return []; } checked.push(extension); - if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) { + if (!extension.manifest.extensionPack || extension.manifest.extensionPack.length === 0) { return []; } - const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1); - const depsOfDeps = []; - for (const dep of dependenciesToUninstall) { - depsOfDeps.push(...this.getDependenciesToUninstallRecursively(dep, installed, checked)); + const packedExtensions = installed.filter(i => extension.manifest.extensionPack.some(id => areSameExtensions({ id }, i.galleryIdentifier))); + const packOfPackedExtensions = []; + for (const packedExtension of packedExtensions) { + packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked)); } - return [...dependenciesToUninstall, ...depsOfDeps]; - } - - private filterDependents(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): ILocalExtension[] { - installed = installed.filter(i => i !== extension && i.manifest.extensionDependencies && i.manifest.extensionDependencies.length > 0); - let result = dependencies.slice(0); - for (let i = 0; i < dependencies.length; i++) { - const dep = dependencies[i]; - const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1); - if (dependents.length) { - result.splice(i - (dependencies.length - result.length), 1); - } - } - return result; + return [...packedExtensions, ...packOfPackedExtensions]; } private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] { - return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1); + return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.galleryIdentifier))); } private doUninstall(extension: ILocalExtension): TPromise { @@ -658,7 +668,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } private preUninstallExtension(extension: ILocalExtension): TPromise { - return pfs.exists(extension.path) + return pfs.exists(extension.location.fsPath) .then(exists => exists ? null : TPromise.wrapError(new Error(nls.localize('notExists', "Could not find extension")))) .then(() => { this.logService.info('Uninstalling extension:', extension.identifier.id); @@ -667,12 +677,19 @@ export class ExtensionManagementService extends Disposable implements IExtension } private uninstallExtension(local: ILocalExtension): TPromise { - // Set all versions of the extension as uninstalled - return this.scanUserExtensions(false) - .then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id: getGalleryExtensionIdFromLocal(local), uuid: local.identifier.uuid })))); + const id = getGalleryExtensionIdFromLocal(local); + let promise = this.uninstallingExtensions.get(id); + if (!promise) { + // Set all versions of the extension as uninstalled + promise = createCancelablePromise(token => this.scanUserExtensions(false) + .then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id, uuid: local.identifier.uuid })))) + .then(() => { this.uninstallingExtensions.delete(id); })); + this.uninstallingExtensions.set(id, promise); + } + return TPromise.wrap(promise); } - private async postUninstallExtension(extension: ILocalExtension, error?: Error): TPromise { + private async postUninstallExtension(extension: ILocalExtension, error?: Error): Promise { if (error) { this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message); } else { @@ -703,7 +720,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private scanSystemExtensions(): TPromise { this.logService.trace('Started scanning system extensions'); - return this.scanExtensions(SystemExtensionsRoot, LocalExtensionType.System) + return this.scanExtensions(this.systemExtensionsPath, LocalExtensionType.System) .then(result => { this.logService.info('Scanned system extensions:', result.length); return result; @@ -746,8 +763,12 @@ export class ExtensionManagementService extends Disposable implements IExtension if (manifest.extensionDependencies) { manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id)); } + if (manifest.extensionPack) { + manifest.extensionPack = manifest.extensionPack.map(id => adoptToGalleryExtensionId(id)); + } const identifier = { id: type === LocalExtensionType.System ? folderName : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null }; - return { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl }; + const galleryIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier.uuid }; + return { type, identifier, galleryIdentifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; })) .then(null, () => null); } @@ -788,7 +809,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private removeExtension(extension: ILocalExtension, type: string): TPromise { this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id); - return pfs.rimraf(extension.path).then(() => this.logService.info('Deleted from disk', extension.identifier.id)); + return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id)); } private isUninstalled(id: string): TPromise { @@ -863,6 +884,10 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } + private toNonCancellablePromise(promise: TPromise): TPromise { + return new TPromise((c, e) => promise.then(result => c(result), error => e(error))); + } + private reportTelemetry(eventName: string, extensionData: any, duration: number, error?: Error): void { const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : void 0; /* __GDPR__ diff --git a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts index a4e1887321f..fccfd884505 100644 --- a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts +++ b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts @@ -37,6 +37,6 @@ export class ExtensionsManifestCache extends Disposable { } invalidate(): void { - pfs.del(this.extensionsManifestCache).done(() => { }, () => { }); + pfs.del(this.extensionsManifestCache).then(() => { }, () => { }); } } diff --git a/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts b/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts new file mode 100644 index 00000000000..065bac07359 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/multiExtensionManagement.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TPromise } from 'vs/base/common/winjs.base'; +import { Event, EventMultiplexer } from 'vs/base/common/event'; +import * as pfs from 'vs/base/node/pfs'; +import { + IExtensionManagementService, ILocalExtension, IGalleryExtension, LocalExtensionType, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, + IExtensionManagementServerService, IExtensionManagementServer, IExtensionGalleryService, InstallOperation +} from 'vs/platform/extensionManagement/common/extensionManagement'; +import { flatten } from 'vs/base/common/arrays'; +import { isWorkspaceExtension, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { URI } from 'vs/base/common/uri'; +import { INotificationService, Severity, INotificationHandle } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { Action } from 'vs/base/common/actions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export class MulitExtensionManagementService extends Disposable implements IExtensionManagementService { + + _serviceBrand: any; + + readonly onInstallExtension: Event; + readonly onDidInstallExtension: Event; + readonly onUninstallExtension: Event; + readonly onDidUninstallExtension: Event; + + private readonly servers: IExtensionManagementServer[]; + private readonly localServer: IExtensionManagementServer; + private readonly otherServers: IExtensionManagementServer[]; + + constructor( + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService, + @INotificationService private notificationService: INotificationService, + @IWindowService private windowService: IWindowService, + @ILogService private logService: ILogService, + @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(); + this.servers = this.extensionManagementServerService.extensionManagementServers; + this.localServer = this.extensionManagementServerService.getLocalExtensionManagementServer(); + this.otherServers = this.servers.filter(s => s !== this.localServer); + + this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onInstallExtension); return emitter; }, new EventMultiplexer())).event; + this.onDidInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer())).event; + this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onUninstallExtension); return emitter; }, new EventMultiplexer())).event; + this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUninstallExtension); return emitter; }, new EventMultiplexer())).event; + + if (this.otherServers.length) { + this.syncExtensions(); + } + } + + getInstalled(type?: LocalExtensionType): TPromise { + return TPromise.join(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type))) + .then(result => flatten(result)); + } + + uninstall(extension: ILocalExtension, force?: boolean): TPromise { + return this.getServer(extension).extensionManagementService.uninstall(extension, force); + } + + reinstallFromGallery(extension: ILocalExtension): TPromise { + return this.getServer(extension).extensionManagementService.reinstallFromGallery(extension); + } + + updateMetadata(extension: ILocalExtension, metadata: IGalleryMetadata): TPromise { + return this.getServer(extension).extensionManagementService.updateMetadata(extension, metadata); + } + + zip(extension: ILocalExtension): TPromise { + throw new Error('Not Supported'); + } + + unzip(zipLocation: URI, type: LocalExtensionType): TPromise { + return TPromise.join(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation, type))).then(() => null); + } + + install(vsix: URI): TPromise { + return this.localServer.extensionManagementService.install(vsix) + .then(extensionIdentifer => this.localServer.extensionManagementService.getInstalled(LocalExtensionType.User) + .then(installed => { + const extension = installed.filter(i => areSameExtensions(i.identifier, extensionIdentifer))[0]; + if (this.otherServers.length && extension && isWorkspaceExtension(extension.manifest, this.configurationService)) { + return TPromise.join(this.otherServers.map(server => server.extensionManagementService.install(vsix))) + .then(() => extensionIdentifer); + } + return extensionIdentifer; + })); + } + + installFromGallery(gallery: IGalleryExtension): TPromise { + if (this.otherServers.length === 0) { + return this.localServer.extensionManagementService.installFromGallery(gallery); + } + return this.extensionGalleryService.getManifest(gallery, CancellationToken.None) + .then(manifest => { + const servers = isWorkspaceExtension(manifest, this.configurationService) ? this.servers : [this.localServer]; + return TPromise.join(servers.map(server => server.extensionManagementService.installFromGallery(gallery))) + .then(() => null); + }); + } + + getExtensionsReport(): TPromise { + return this.extensionManagementServerService.getLocalExtensionManagementServer().extensionManagementService.getExtensionsReport(); + } + + private getServer(extension: ILocalExtension): IExtensionManagementServer { + return this.extensionManagementServerService.getExtensionManagementServer(extension.location); + } + + private async syncExtensions(): Promise { + this.localServer.extensionManagementService.getInstalled(LocalExtensionType.User) + .then(async localExtensions => { + const workspaceExtensions = localExtensions.filter(e => isWorkspaceExtension(e.manifest, this.configurationService)); + const extensionsToSync: Map = await this.getExtensionsToSync(workspaceExtensions); + if (extensionsToSync.size > 0) { + const handler = this.notificationService.notify({ severity: Severity.Info, message: localize('synchronising', "Synchronising workspace extensions...") }); + handler.progress.infinite(); + this.doSyncExtensions(extensionsToSync, handler).then(() => { + handler.progress.done(); + handler.updateMessage(localize('Synchronize.finished', "Finished synchronising. Please reload now.")); + handler.updateActions({ + primary: [ + new Action('Synchronize.reloadNow', localize('Synchronize.reloadNow', "Reload Now"), null, true, () => this.windowService.reloadWindow()) + ] + }); + }, error => { + handler.progress.done(); + handler.updateSeverity(Severity.Error); + handler.updateMessage(error); + }); + } + }, err => this.logService.error('Error while Synchronisation', err)); + } + + private async getExtensionsToSync(workspaceExtensions: ILocalExtension[]): Promise> { + const extensionsToSync: Map = new Map(); + for (const server of this.otherServers) { + const extensions = await server.extensionManagementService.getInstalled(LocalExtensionType.User); + const groupedByVersionId: Map = extensions.reduce((groupedById, extension) => groupedById.set(`${extension.galleryIdentifier.id}-${extension.manifest.version}`, extension), new Map()); + const toSync = workspaceExtensions.filter(e => !groupedByVersionId.has(`${e.galleryIdentifier.id}-${e.manifest.version}`)); + if (toSync.length) { + extensionsToSync.set(server, toSync); + } + } + return extensionsToSync; + } + + private async doSyncExtensions(extensionsToSync: Map, notificationHandler: INotificationHandle): Promise { + const ids: string[] = []; + const zipLocationResolvers: TPromise<{ location: URI, vsix: boolean }>[] = []; + + extensionsToSync.forEach(extensions => { + for (const extension of extensions) { + if (ids.indexOf(extension.galleryIdentifier.id) === -1) { + ids.push(extension.galleryIdentifier.id); + zipLocationResolvers.push(this.downloadFromGallery(extension) + .then(location => location ? { location, vsix: true } : this.localServer.extensionManagementService.zip(extension).then(location => ({ location, vsix: false })))); + } + } + }); + + const zipLocations = await TPromise.join(zipLocationResolvers); + const promises: Promise[] = []; + extensionsToSync.forEach((extensions, server) => { + let promise: Promise = Promise.resolve(); + extensions.forEach(extension => { + const index = ids.indexOf(extension.galleryIdentifier.id); + const { location, vsix } = zipLocations[index]; + promise = promise + .then(() => { + notificationHandler.updateMessage(localize('synchronising extension', "Synchronising workspace extension: {0}", extension.manifest.displayName || extension.manifest.name)); + return vsix ? server.extensionManagementService.install(location) : server.extensionManagementService.unzip(location, extension.type); + }).then( + () => pfs.rimraf(location.fsPath), + error => pfs.rimraf(location.fsPath).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error))); + }); + promises.push(promise); + }); + + await Promise.all(promises); + } + + private downloadFromGallery(extension: ILocalExtension): TPromise { + if (this.extensionGalleryService.isEnabled()) { + return this.extensionGalleryService.getExtension(extension.galleryIdentifier, extension.manifest.version) + .then(galleryExtension => galleryExtension ? this.extensionGalleryService.download(galleryExtension, InstallOperation.None).then(location => URI.file(location)) : null); + } + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index 717a17dd3b0..655d4b05b50 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -35,10 +35,11 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { constructor(instantiationService: TestInstantiationService) { super(storageService(instantiationService), instantiationService.get(IWorkspaceContextService), instantiationService.get(IEnvironmentService) || instantiationService.stub(IEnvironmentService, {} as IEnvironmentService), - instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: new Emitter() })); + instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, + { onDidUninstallExtension: new Emitter().event } as IExtensionManagementService)); } - public reset(): TPromise { + public async reset(): Promise { return this.getDisabledExtensions().then(extensions => extensions.forEach(d => this.setEnablement(aLocalExtension(d.id), EnablementState.Enabled))); } } @@ -52,7 +53,7 @@ suite('ExtensionEnablementService Test', () => { setup(() => { instantiationService = new TestInstantiationService(); - instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event }); + instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([]) } as IExtensionManagementService); testObject = new TestExtensionEnablementService(instantiationService); }); @@ -331,6 +332,12 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); + test('test canChangeEnablement return false when the extension is disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); + }); + test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => { instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -338,12 +345,32 @@ suite('ExtensionEnablementService Test', () => { extension.type = LocalExtensionType.System; assert.equal(testObject.canChangeEnablement(extension), true); }); + + test('test canChangeEnablement return false for system extensions when extension is disabled in environment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + testObject = new TestExtensionEnablementService(instantiationService); + const extension = aLocalExtension('pub.a'); + extension.type = LocalExtensionType.System; + assert.equal(testObject.canChangeEnablement(extension), true); + }); + + test('test getDisabledExtensions include extensions disabled in enviroment', () => { + instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([aLocalExtension('pub.a'), aLocalExtension('pub.b')]) } as IExtensionManagementService); + testObject = new TestExtensionEnablementService(instantiationService); + return testObject.getDisabledExtensions() + .then(actual => { + assert.equal(actual.length, 1); + assert.equal(actual[0].id, 'pub.a'); + }); + }); }); function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension { const [publisher, name] = id.split('.'); return Object.create({ identifier: { id }, + galleryIdentifier: { id, uuid: void 0 }, manifest: { name, publisher, diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index b59c623ee3c..abe84da07b4 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as paths from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as glob from 'vs/base/common/glob'; import { isLinux } from 'vs/base/common/platform'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -128,16 +128,11 @@ export interface IFileService { createFolder(resource: URI): TPromise; /** - * Renames the provided file to use the new name. The returned promise - * will have the stat model object as a result. + * Deletes the provided file. The optional useTrash parameter allows to + * move the file to trash. The optional recursive parameter allows to delete + * non-empty folders recursively. */ - rename(resource: URI, newName: string): TPromise; - - /** - * Deletes the provided file. The optional useTrash parameter allows to - * move the file to trash. - */ - del(resource: URI, useTrash?: boolean): TPromise; + del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise; /** * Allows to start a watcher that reports file change events on the provided resource. @@ -164,6 +159,10 @@ export interface FileWriteOptions { create: boolean; } +export interface FileDeleteOptions { + recursive: boolean; +} + export enum FileType { Unknown = 0, File = 1, @@ -183,12 +182,13 @@ export interface IWatchOptions { excludes: string[]; } -export enum FileSystemProviderCapabilities { +export const enum FileSystemProviderCapabilities { FileReadWrite = 1 << 1, FileOpenReadWriteClose = 1 << 2, FileFolderCopy = 1 << 3, - PathCaseSensitive = 1 << 10 + PathCaseSensitive = 1 << 10, + Readonly = 1 << 11 } export interface IFileSystemProvider { @@ -198,21 +198,21 @@ export interface IFileSystemProvider { onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; - stat(resource: URI): TPromise; - mkdir(resource: URI): TPromise; - readdir(resource: URI): TPromise<[string, FileType][]>; - delete(resource: URI): TPromise; + stat(resource: URI): Thenable; + mkdir(resource: URI): Thenable; + readdir(resource: URI): Thenable<[string, FileType][]>; + delete(resource: URI, opts: FileDeleteOptions): Thenable; - rename(from: URI, to: URI, opts: FileOverwriteOptions): TPromise; - copy?(from: URI, to: URI, opts: FileOverwriteOptions): TPromise; + rename(from: URI, to: URI, opts: FileOverwriteOptions): Thenable; + copy?(from: URI, to: URI, opts: FileOverwriteOptions): Thenable; - readFile?(resource: URI): TPromise; - writeFile?(resource: URI, content: Uint8Array, opts: FileWriteOptions): TPromise; + readFile?(resource: URI): Thenable; + writeFile?(resource: URI, content: Uint8Array, opts: FileWriteOptions): Thenable; - open?(resource: URI): TPromise; - close?(fd: number): TPromise; - read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): TPromise; - write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): TPromise; + open?(resource: URI): Thenable; + close?(fd: number): Thenable; + read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Thenable; + write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Thenable; } export interface IFileSystemProviderRegistrationEvent { @@ -221,7 +221,7 @@ export interface IFileSystemProviderRegistrationEvent { provider?: IFileSystemProvider; } -export enum FileOperation { +export const enum FileOperation { CREATE, DELETE, MOVE, @@ -233,15 +233,15 @@ export class FileOperationEvent { constructor(private _resource: URI, private _operation: FileOperation, private _target?: IFileStat) { } - public get resource(): URI { + get resource(): URI { return this._resource; } - public get target(): IFileStat { + get target(): IFileStat { return this._target; } - public get operation(): FileOperation { + get operation(): FileOperation { return this._operation; } } @@ -249,7 +249,7 @@ export class FileOperationEvent { /** * Possible changes that can occur to a file. */ -export enum FileChangeType { +export const enum FileChangeType { UPDATED = 0, ADDED = 1, DELETED = 2 @@ -279,27 +279,29 @@ export class FileChangesEvent { this._changes = changes; } - public get changes() { + get changes() { return this._changes; } /** - * Returns true if this change event contains the provided file with the given change type. In case of + * Returns true if this change event contains the provided file with the given change type (if provided). In case of * type DELETED, this method will also return true if a folder got deleted that is the parent of the * provided file path. */ - public contains(resource: URI, type: FileChangeType): boolean { + contains(resource: URI, type?: FileChangeType): boolean { if (!resource) { return false; } + const checkForChangeType = !isUndefinedOrNull(type); + return this._changes.some(change => { - if (change.type !== type) { + if (checkForChangeType && change.type !== type) { return false; } // For deleted also return true when deleted folder is parent of target path - if (type === FileChangeType.DELETED) { + if (change.type === FileChangeType.DELETED) { return isEqualOrParent(resource, change.resource, !isLinux /* ignorecase */); } @@ -310,42 +312,42 @@ export class FileChangesEvent { /** * Returns the changes that describe added files. */ - public getAdded(): IFileChange[] { + getAdded(): IFileChange[] { return this.getOfType(FileChangeType.ADDED); } /** * Returns if this event contains added files. */ - public gotAdded(): boolean { + gotAdded(): boolean { return this.hasType(FileChangeType.ADDED); } /** * Returns the changes that describe deleted files. */ - public getDeleted(): IFileChange[] { + getDeleted(): IFileChange[] { return this.getOfType(FileChangeType.DELETED); } /** * Returns if this event contains deleted files. */ - public gotDeleted(): boolean { + gotDeleted(): boolean { return this.hasType(FileChangeType.DELETED); } /** * Returns the changes that describe updated files. */ - public getUpdated(): IFileChange[] { + getUpdated(): IFileChange[] { return this.getOfType(FileChangeType.UPDATED); } /** * Returns if this event contains updated files. */ - public gotUpdated(): boolean { + gotUpdated(): boolean { return this.hasType(FileChangeType.UPDATED); } @@ -404,6 +406,11 @@ export interface IBaseStat { * current state of the file or directory. */ etag: string; + + /** + * The resource is readonly. + */ + isReadonly?: boolean; } /** @@ -615,7 +622,7 @@ export class FileOperationError extends Error { } } -export enum FileOperationResult { +export const enum FileOperationResult { FILE_IS_BINARY, FILE_IS_DIRECTORY, FILE_NOT_FOUND, diff --git a/src/vs/platform/files/test/files.test.ts b/src/vs/platform/files/test/files.test.ts index aba5148b53d..2b5d6e238c4 100644 --- a/src/vs/platform/files/test/files.test.ts +++ b/src/vs/platform/files/test/files.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { join, isEqual, isEqualOrParent } from 'vs/base/common/paths'; import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/platform/history/common/history.ts b/src/vs/platform/history/common/history.ts index 1ce03f870fc..3e75536f968 100644 --- a/src/vs/platform/history/common/history.ts +++ b/src/vs/platform/history/common/history.ts @@ -9,12 +9,13 @@ import { IPath } from 'vs/platform/windows/common/windows'; import { Event as CommonEvent } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; export const IHistoryMainService = createDecorator('historyMainService'); export interface IRecentlyOpened { workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; - files: string[]; + files: URI[]; } export interface IHistoryMainService { @@ -22,9 +23,9 @@ export interface IHistoryMainService { onRecentlyOpenedChange: CommonEvent; - addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void; + addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: URI[]): void; getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened; - removeFromRecentlyOpened(paths: string[]): void; + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[]): void; clearRecentlyOpened(): void; updateWindowsJumpList(): void; diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 5c8c223322b..555e4af8546 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -5,22 +5,33 @@ 'use strict'; -import * as path from 'path'; import * as nls from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; -import { trim } from 'vs/base/common/strings'; import { IStateService } from 'vs/platform/state/common/state'; import { app } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; -import { getPathLabel, getBaseLabel } from 'vs/base/common/labels'; +import { getBaseLabel } 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, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceSavedEvent } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspacesMainService, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isEqual } from 'vs/base/common/paths'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { getComparisonKey, isEqual as areResourcesEqual, dirname } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { ILabelService } from 'vs/platform/label/common/label'; + +interface ISerializedRecentlyOpened { + workspaces2: (IWorkspaceIdentifier | string)[]; // IWorkspaceIdentifier or URI.toString() + files2: string[]; // files as URI.toString() +} + +interface ILegacySerializedRecentlyOpened { + workspaces: (IWorkspaceIdentifier | string | UriComponents)[]; // legacy (UriComponents was also supported for a few insider builds) + files: string[]; // files as paths +} export class HistoryMainService implements IHistoryMainService { @@ -40,7 +51,7 @@ export class HistoryMainService implements IHistoryMainService { @IStateService private stateService: IStateService, @ILogService private logService: ILogService, @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService, - @IEnvironmentService private environmentService: IEnvironmentService, + @ILabelService private labelService: ILabelService ) { this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800); @@ -49,6 +60,7 @@ export class HistoryMainService implements IHistoryMainService { private registerListeners(): void { this.workspacesMainService.onWorkspaceSaved(e => this.onWorkspaceSaved(e)); + this.labelService.onDidRegisterFormatter(() => this._onRecentlyOpenedChange.fire()); } private onWorkspaceSaved(e: IWorkspaceSavedEvent): void { @@ -57,34 +69,38 @@ export class HistoryMainService implements IHistoryMainService { this.addRecentlyOpened([e.workspace], []); } - public addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void { + addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: URI[]): void { if ((workspaces && workspaces.length > 0) || (files && files.length > 0)) { const mru = this.getRecentlyOpened(); // Workspaces - workspaces.forEach(workspace => { - const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); - if (isUntitledWorkspace) { - return; // only store saved workspaces - } + if (Array.isArray(workspaces)) { + workspaces.forEach(workspace => { + const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace); + if (isUntitledWorkspace) { + return; // only store saved workspaces + } - mru.workspaces.unshift(workspace); - mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); + mru.workspaces.unshift(workspace); + mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace)); - // We do not add to recent documents here because on Windows we do this from a custom - // JumpList and on macOS we fill the recent documents in one go from all our data later. - }); + // We do not add to recent documents here because on Windows we do this from a custom + // JumpList and on macOS we fill the recent documents in one go from all our data later. + }); + } // Files - files.forEach((path) => { - mru.files.unshift(path); - mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); + if (Array.isArray(files)) { + files.forEach((fileUri) => { + mru.files.unshift(fileUri); + mru.files = arrays.distinct(mru.files, file => this.distinctFn(file)); - // Add to recent documents (Windows only, macOS later) - if (isWindows) { - app.addRecentDocument(path); - } - }); + // Add to recent documents (Windows only, macOS later) + if (isWindows && fileUri.scheme === Schemas.file) { + app.addRecentDocument(fileUri.fsPath); + } + }); + } // Make sure its bounded mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES); @@ -100,21 +116,40 @@ export class HistoryMainService implements IHistoryMainService { } } - public removeFromRecentlyOpened(pathsToRemove: string[]): void { + removeFromRecentlyOpened(pathsToRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[]): void { const mru = this.getRecentlyOpened(); let update = false; pathsToRemove.forEach((pathToRemove => { // Remove workspace - let index = arrays.firstIndex(mru.workspaces, workspace => isEqual(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath, pathToRemove, !isLinux /* ignorecase */)); + let index = arrays.firstIndex(mru.workspaces, workspace => { + if (isWorkspaceIdentifier(pathToRemove)) { + return isWorkspaceIdentifier(workspace) && isEqual(pathToRemove.configPath, workspace.configPath, !isLinux /* ignorecase */); + } + if (isSingleFolderWorkspaceIdentifier(pathToRemove)) { + return isSingleFolderWorkspaceIdentifier(workspace) && areResourcesEqual(pathToRemove, workspace); + } + if (typeof pathToRemove === 'string') { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + return workspace.scheme === Schemas.file && areResourcesEqual(URI.file(pathToRemove), workspace); + } + if (isWorkspaceIdentifier(workspace)) { + return isEqual(pathToRemove, workspace.configPath, !isLinux /* ignorecase */); + } + } + return false; + }); if (index >= 0) { mru.workspaces.splice(index, 1); update = true; } // Remove file - index = arrays.firstIndex(mru.files, file => isEqual(file, pathToRemove, !isLinux /* ignorecase */)); + const pathToRemoveURI = pathToRemove instanceof URI ? pathToRemove : typeof pathToRemove === 'string' ? URI.file(pathToRemove) : null; + if (pathToRemoveURI) { + index = arrays.firstIndex(mru.files, file => areResourcesEqual(file, pathToRemoveURI)); + } if (index >= 0) { mru.files.splice(index, 1); update = true; @@ -149,20 +184,31 @@ export class HistoryMainService implements IHistoryMainService { let maxEntries = HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES; // Take up to maxEntries/2 workspaces - for (let i = 0; i < mru.workspaces.length && i < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) { + let nEntries = 0; + for (let i = 0; i < mru.workspaces.length && nEntries < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) { const workspace = mru.workspaces[i]; - app.addRecentDocument(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath); - maxEntries--; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + if (workspace.scheme === Schemas.file) { + app.addRecentDocument(workspace.fsPath); + nEntries++; + } + } else { + app.addRecentDocument(workspace.configPath); + nEntries++; + } } // Take up to maxEntries files - for (let i = 0; i < mru.files.length && i < maxEntries; i++) { + for (let i = 0; i < mru.files.length && nEntries < maxEntries; i++) { const file = mru.files[i]; - app.addRecentDocument(file); + if (file.scheme === Schemas.file) { + app.addRecentDocument(file.fsPath); + nEntries++; + } } } - public clearRecentlyOpened(): void { + clearRecentlyOpened(): void { this.saveRecentlyOpened({ workspaces: [], files: [] }); app.clearRecentDocuments(); @@ -170,12 +216,12 @@ export class HistoryMainService implements IHistoryMainService { this._onRecentlyOpenedChange.fire(); } - public getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened { + getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened { let workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; - let files: string[]; + let files: URI[]; // Get from storage - const storedRecents = this.stateService.getItem(HistoryMainService.recentlyOpenedStorageKey); + const storedRecents = this.getRecentlyOpenedFromStorage(); if (storedRecents) { workspaces = storedRecents.workspaces || []; files = storedRecents.files || []; @@ -191,7 +237,7 @@ export class HistoryMainService implements IHistoryMainService { // Add currently files to open to the beginning if any if (currentFiles) { - files.unshift(...currentFiles.map(f => f.filePath)); + files.unshift(...currentFiles.map(f => f.fileUri)); } // Clear those dupes @@ -204,19 +250,72 @@ export class HistoryMainService implements IHistoryMainService { return { workspaces, files }; } - private distinctFn(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string): string { - if (isSingleFolderWorkspaceIdentifier(workspaceOrFile)) { - return isLinux ? workspaceOrFile : workspaceOrFile.toLowerCase(); + private distinctFn(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): string { + if (workspaceOrFile instanceof URI) { + return getComparisonKey(workspaceOrFile); } - return workspaceOrFile.id; } - private saveRecentlyOpened(recent: IRecentlyOpened): void { - this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, recent); + private getRecentlyOpenedFromStorage(): IRecentlyOpened { + const storedRecents = this.stateService.getItem(HistoryMainService.recentlyOpenedStorageKey); + const result: IRecentlyOpened = { workspaces: [], files: [] }; + if (storedRecents) { + if (Array.isArray(storedRecents.workspaces2)) { + for (const workspace of storedRecents.workspaces2) { + if (isWorkspaceIdentifier(workspace)) { + result.workspaces.push(workspace); + } else if (typeof workspace === 'string') { + result.workspaces.push(URI.parse(workspace)); + } + } + } else if (Array.isArray(storedRecents.workspaces)) { + // TODO@martin legacy support can be removed at some point (6 month?) + // format of 1.25 and before + for (const workspace of storedRecents.workspaces) { + if (typeof workspace === 'string') { + result.workspaces.push(URI.file(workspace)); + } else if (isWorkspaceIdentifier(workspace)) { + result.workspaces.push(workspace); + } else if (workspace && typeof workspace.path === 'string' && typeof workspace.scheme === 'string') { + // added by 1.26-insiders + result.workspaces.push(URI.revive(workspace)); + } + } + } + if (Array.isArray(storedRecents.files2)) { + for (const file of storedRecents.files2) { + if (typeof file === 'string') { + result.files.push(URI.parse(file)); + } + } + } else if (Array.isArray(storedRecents.files)) { + for (const file of storedRecents.files) { + if (typeof file === 'string') { + result.files.push(URI.file(file)); + } + } + } + } + return result; } - public updateWindowsJumpList(): void { + private saveRecentlyOpened(recent: IRecentlyOpened): void { + const serialized: ISerializedRecentlyOpened = { workspaces2: [], files2: [] }; + for (const workspace of recent.workspaces) { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + serialized.workspaces2.push(workspace.toString()); + } else { + serialized.workspaces2.push(workspace); + } + } + for (const file of recent.files) { + serialized.files2.push(file.toString()); + } + this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, serialized); + } + + updateWindowsJumpList(): void { if (!isWindows) { return; // only on windows } @@ -246,22 +345,44 @@ export class HistoryMainService implements IHistoryMainService { // so we need to update our list of recent paths with the choice of the user to not add them again // Also: Windows will not show our custom category at all if there is any entry which was removed // by the user! See https://github.com/Microsoft/vscode/issues/15052 - this.removeFromRecentlyOpened(app.getJumpListSettings().removedItems.filter(r => !!r.args).map(r => trim(r.args, '"'))); + let toRemove: (ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier)[] = []; + for (let item of app.getJumpListSettings().removedItems) { + const args = item.args; + if (args) { + const match = /^--folder-uri\s+"([^"]+)"$/.exec(args); + if (match) { + if (args[0] === '-') { + toRemove.push(URI.parse(match[1])); + } else { + let configPath = match[1]; + toRemove.push({ id: this.workspacesMainService.getWorkspaceId(configPath), configPath }); + } + } + } + } + this.removeFromRecentlyOpened(toRemove); // Add entries jumpList.push({ type: 'custom', name: nls.localize('recentFolders', "Recent Workspaces"), items: this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => { - const title = isSingleFolderWorkspaceIdentifier(workspace) ? getBaseLabel(workspace) : getWorkspaceLabel(workspace, this.environmentService); - const description = isSingleFolderWorkspaceIdentifier(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(path.dirname(workspace))) : nls.localize('codeWorkspace', "Code Workspace"); - + const title = this.labelService.getWorkspaceLabel(workspace); + let description; + let args; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), this.labelService.getUriLabel(dirname(workspace))); + args = `--folder-uri "${workspace.toString()}"`; + } else { + description = nls.localize('codeWorkspace', "Code Workspace"); + args = `"${workspace.configPath}"`; + } return { type: 'task', title, description, program: process.execPath, - args: `"${isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath}"`, // open folder (use quotes to support paths with whitespaces) + args, iconPath: 'explorer.exe', // simulate folder icon iconIndex: 0 }; @@ -280,4 +401,4 @@ export class HistoryMainService implements IHistoryMainService { this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors } } -} \ No newline at end of file +} diff --git a/src/vs/platform/integrity/common/integrity.ts b/src/vs/platform/integrity/common/integrity.ts index 6984b0fa8e7..c2ffaae674b 100644 --- a/src/vs/platform/integrity/common/integrity.ts +++ b/src/vs/platform/integrity/common/integrity.ts @@ -5,7 +5,7 @@ 'use strict'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export const IIntegrityService = createDecorator('integrityService'); diff --git a/src/vs/platform/integrity/node/integrityServiceImpl.ts b/src/vs/platform/integrity/node/integrityServiceImpl.ts index 5a45065e013..5174f67fdeb 100644 --- a/src/vs/platform/integrity/node/integrityServiceImpl.ts +++ b/src/vs/platform/integrity/node/integrityServiceImpl.ts @@ -10,7 +10,7 @@ import * as crypto from 'crypto'; import { TPromise } from 'vs/base/common/winjs.base'; import { IIntegrityService, IntegrityTestResult, ChecksumPair } from 'vs/platform/integrity/common/integrity'; import product from 'vs/platform/node/product'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -133,7 +133,7 @@ export class IntegrityServiceImpl implements IIntegrityService { private _resolve(filename: string, expected: string): TPromise { let fileUri = URI.parse(require.toUrl(filename)); - return new TPromise((c, e, p) => { + return new TPromise((c, e) => { fs.readFile(fileUri.fsPath, (err, buff) => { if (err) { return e(err); diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 46d378dc8d4..49de26922cc 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -22,7 +22,7 @@ export interface WindowData { zoomLevel: number; } -export enum IssueType { +export const enum IssueType { Bug, PerformanceIssue, FeatureRequest, @@ -74,6 +74,7 @@ export interface ProcessExplorerStyles extends WindowStyles { } export interface ProcessExplorerData extends WindowData { + pid: number; styles: ProcessExplorerStyles; } diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 0129b71e68d..bef6d5a627b 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -10,9 +10,9 @@ import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; -import { BrowserWindow, ipcMain, screen } from 'electron'; -import { ILaunchService } from 'vs/code/electron-main/launch'; -import { getPerformanceInfo, PerformanceInfo, getSystemInfo, SystemInfo } from 'vs/code/electron-main/diagnostics'; +import { BrowserWindow, ipcMain, screen, Event } from 'electron'; +import { ILaunchService } from 'vs/platform/launch/electron-main/launchService'; +import { PerformanceInfo, SystemInfo, IDiagnosticsService } from 'vs/platform/diagnostics/electron-main/diagnosticsService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -31,25 +31,32 @@ export class IssueService implements IIssueService { @IEnvironmentService private environmentService: IEnvironmentService, @ILaunchService private launchService: ILaunchService, @ILogService private logService: ILogService, + @IDiagnosticsService private diagnosticsService: IDiagnosticsService ) { } openReporter(data: IssueReporterData): TPromise { - ipcMain.on('issueSystemInfoRequest', event => { + ipcMain.on('vscode:issueSystemInfoRequest', (event: Event) => { this.getSystemInformation().then(msg => { - event.sender.send('issueSystemInfoResponse', msg); + event.sender.send('vscode:issueSystemInfoResponse', msg); }); }); - ipcMain.on('issuePerformanceInfoRequest', event => { + ipcMain.on('vscode:issuePerformanceInfoRequest', (event: Event) => { this.getPerformanceInfo().then(msg => { - event.sender.send('issuePerformanceInfoResponse', msg); + event.sender.send('vscode:issuePerformanceInfoResponse', msg); }); }); - ipcMain.on('workbenchCommand', (event, arg) => { + ipcMain.on('vscode:workbenchCommand', (event, arg) => { this._issueParentWindow.webContents.send('vscode:runAction', { id: arg, from: 'issueReporter' }); }); + ipcMain.on('vscode:closeIssueReporter', (event: Event) => { + if (this._issueWindow) { + this._issueWindow.close(); + } + }); + this._issueParentWindow = BrowserWindow.getFocusedWindow(); const position = this.getWindowPosition(this._issueParentWindow, 700, 800); if (!this._issueWindow) { @@ -75,8 +82,10 @@ export class IssueService implements IIssueService { this._issueWindow.on('close', () => this._issueWindow = null); this._issueParentWindow.on('closed', () => { - this._issueWindow.close(); - this._issueWindow = null; + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } }); } @@ -86,9 +95,9 @@ export class IssueService implements IIssueService { } openProcessExplorer(data: ProcessExplorerData): TPromise { - ipcMain.on('windowsInfoRequest', event => { + ipcMain.on('windowsInfoRequest', (event: Event) => { this.launchService.getMainProcessInfo().then(info => { - event.sender.send('windowsInfoResponse', info.windows); + event.sender.send('vscode:windowsInfoResponse', info.windows); }); }); @@ -133,8 +142,10 @@ export class IssueService implements IIssueService { this._processExplorerWindow.on('close', () => this._processExplorerWindow = void 0); parentWindow.on('close', () => { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } }); } @@ -217,7 +228,7 @@ export class IssueService implements IIssueService { private getSystemInformation(): TPromise { return new Promise((resolve, reject) => { this.launchService.getMainProcessInfo().then(info => { - resolve(getSystemInfo(info)); + resolve(this.diagnosticsService.getSystemInfo(info)); }); }); } @@ -225,7 +236,7 @@ export class IssueService implements IIssueService { private getPerformanceInfo(): TPromise { return new Promise((resolve, reject) => { this.launchService.getMainProcessInfo().then(info => { - getPerformanceInfo(info) + this.diagnosticsService.getPerformanceInfo(info) .then(diagnosticInfo => { resolve(diagnosticInfo); }) diff --git a/src/vs/platform/issue/common/issueIpc.ts b/src/vs/platform/issue/node/issueIpc.ts similarity index 87% rename from src/vs/platform/issue/common/issueIpc.ts rename to src/vs/platform/issue/node/issueIpc.ts index d804c6579fe..e58dff508b8 100644 --- a/src/vs/platform/issue/common/issueIpc.ts +++ b/src/vs/platform/issue/node/issueIpc.ts @@ -6,8 +6,9 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IIssueService, IssueReporterData, ProcessExplorerData } from './issue'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IIssueService, IssueReporterData, ProcessExplorerData } from '../common/issue'; +import { Event } from 'vs/base/common/event'; export interface IIssueChannel extends IChannel { call(command: 'openIssueReporter', arg: IssueReporterData): TPromise; @@ -19,6 +20,10 @@ export class IssueChannel implements IIssueChannel { constructor(private service: IIssueService) { } + listen(event: string): Event { + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'openIssueReporter': diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index f001258b68e..6286f886142 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -5,7 +5,7 @@ 'use strict'; import * as nls from 'vs/nls'; -import { ResolvedKeybinding, Keybinding } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding, Keybinding, KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { KeybindingResolver, IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; @@ -188,9 +188,11 @@ export abstract class AbstractKeybindingService extends Disposable implements IK if (!resolveResult.bubble) { shouldPreventDefault = true; } - this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).done(undefined, err => { - this._notificationService.warn(err); - }); + if (typeof resolveResult.commandArgs === 'undefined') { + this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err)); + } else { + this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err)); + } /* __GDPR__ "workbenchActionExecuted" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -202,4 +204,18 @@ export abstract class AbstractKeybindingService extends Disposable implements IK return shouldPreventDefault; } + + mightProducePrintableCharacter(event: IKeyboardEvent): boolean { + if (event.ctrlKey || event.metaKey) { + // ignore ctrl/cmd-combination but not shift/alt-combinatios + return false; + } + // weak check for certain ranges. this is properly implemented in a subclass + // with access to the KeyboardMapperFactory. + if ((event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) + || (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9)) { + return true; + } + return false; + } } diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index c491ec7a8d0..ea2de529888 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -18,7 +18,7 @@ export interface IUserFriendlyKeybinding { when?: string; } -export enum KeybindingSource { +export const enum KeybindingSource { Default = 1, User } @@ -77,5 +77,11 @@ export interface IKeybindingService { getKeybindings(): ResolvedKeybindingItem[]; customKeybindingsCount(): number; + + /** + * Will the given key event produce a character that's rendered on screen, e.g. in a + * text box. *Note* that the results of this function can be incorrect. + */ + mightProducePrintableCharacter(event: IKeyboardEvent): boolean; } diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 39113558334..3d581b4e64f 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -57,6 +57,14 @@ export const enum KeybindingRuleSource { Extension = 1 } +export const enum KeybindingWeight { + EditorCore = 0, + EditorContrib = 100, + WorkbenchContrib = 200, + BuiltinExtension = 300, + ExternalExtension = 400 +} + export interface ICommandAndKeybindingRule extends IKeybindingRule { handler: ICommandHandler; description?: ICommandHandlerDescription; @@ -67,14 +75,6 @@ export interface IKeybindingsRegistry { registerKeybindingRule2(rule: IKeybindingRule2, source?: KeybindingRuleSource): void; registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule, source?: KeybindingRuleSource): void; getDefaultKeybindings(): IKeybindingItem[]; - - WEIGHT: { - editorCore(importance?: number): number; - editorContrib(importance?: number): number; - workbenchContrib(importance?: number): number; - builtinExtension(importance?: number): number; - externalExtension(importance?: number): number; - }; } class KeybindingsRegistryImpl implements IKeybindingsRegistry { @@ -82,24 +82,6 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { private _keybindings: IKeybindingItem[]; private _keybindingsSorted: boolean; - public WEIGHT = { - editorCore: (importance: number = 0): number => { - return 0 + importance; - }, - editorContrib: (importance: number = 0): number => { - return 100 + importance; - }, - workbenchContrib: (importance: number = 0): number => { - return 200 + importance; - }, - builtinExtension: (importance: number = 0): number => { - return 300 + importance; - }, - externalExtension: (importance: number = 0): number => { - return 400 + importance; - } - }; - constructor() { this._keybindings = []; this._keybindingsSorted = true; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index 19f285354cc..25dc83e6d4d 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -55,24 +55,24 @@ suite('KeybindingLabels', () => { assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A'); assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A'); assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Windows+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Super+A'); // two modifiers assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A'); assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Windows+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Super+A'); assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Windows+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Windows+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Super+A'); // three modifiers assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Windows+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Windows+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Windows+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Super+A'); // four modifiers - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Windows+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); // chord assertUSLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B'); @@ -122,7 +122,7 @@ suite('KeybindingLabels', () => { } assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); - assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); + assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Super+A'); assertAriaLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Command+A'); }); diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 8a44c7bd8db..a8b00dfb030 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -124,4 +124,8 @@ export class MockKeybindingService implements IKeybindingService { dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { return false; } + + mightProducePrintableCharacter(e: IKeyboardEvent): boolean { + return false; + } } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts new file mode 100644 index 00000000000..084c063627a --- /dev/null +++ b/src/vs/platform/label/common/label.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { isEqual, basenameOrAuthority } from 'vs/base/common/resources'; +import { isLinux, isWindows } from 'vs/base/common/platform'; +import { tildify, getPathLabel } from 'vs/base/common/labels'; +import { ltrim } from 'vs/base/common/strings'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { localize } from 'vs/nls'; +import { isParent } from 'vs/platform/files/common/files'; +import { basename, dirname, join } from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; + +export interface RegisterFormatterEvent { + scheme: string; + formatter: LabelRules; +} + +export interface ILabelService { + _serviceBrand: any; + getUriLabel(resource: URI, relative?: boolean, forceNoTildify?: boolean): string; + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; + registerFormatter(schema: string, formatter: LabelRules): IDisposable; + onDidRegisterFormatter: Event; +} + +export interface LabelRules { + uri: { + label: string; // myLabel:/${path} + separator: '/' | '\\' | ''; + tildify?: boolean; + normalizeDriveLetter?: boolean; + }; + workspace?: { + suffix: string; + }; +} + +const LABEL_SERVICE_ID = 'label'; +const sepRegexp = /\//g; +const labelMatchingRegexp = /\$\{scheme\}|\$\{authority\}|\$\{path\}/g; + +function hasDriveLetter(path: string): boolean { + return isWindows && path && path[2] === ':'; +} + +export class LabelService implements ILabelService { + _serviceBrand: any; + + private readonly formatters = new Map(); + private readonly _onDidRegisterFormatter = new Emitter(); + + constructor( + @IEnvironmentService private environmentService: IEnvironmentService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { } + + get onDidRegisterFormatter(): Event { + return this._onDidRegisterFormatter.event; + } + + getUriLabel(resource: URI, relative?: boolean, forceNoTildify?: boolean): string { + if (!resource) { + return undefined; + } + const formatter = this.formatters.get(resource.scheme); + if (!formatter) { + return getPathLabel(resource.path, this.environmentService, relative ? this.contextService : undefined); + } + + if (relative) { + const baseResource = this.contextService && this.contextService.getWorkspaceFolder(resource); + if (baseResource) { + let relativeLabel: string; + if (isEqual(baseResource.uri, resource, !isLinux)) { + relativeLabel = ''; // no label if resources are identical + } else { + const baseResourceLabel = this.formatUri(baseResource.uri, formatter, forceNoTildify); + relativeLabel = ltrim(this.formatUri(resource, formatter, forceNoTildify).substring(baseResourceLabel.length), formatter.uri.separator); + } + + const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; + if (hasMultipleRoots) { + const rootName = (baseResource && baseResource.name) ? baseResource.name : basenameOrAuthority(baseResource.uri); + relativeLabel = relativeLabel ? (rootName + ' • ' + relativeLabel) : rootName; // always show root basename if there are multiple + } + + return relativeLabel; + } + } + + return this.formatUri(resource, formatter, forceNoTildify); + } + + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string { + if (!isWorkspaceIdentifier(workspace) && !isSingleFolderWorkspaceIdentifier(workspace)) { + workspace = toWorkspaceIdentifier(workspace); + if (!workspace) { + return ''; + } + } + + // Workspace: Single Folder + if (isSingleFolderWorkspaceIdentifier(workspace)) { + // Folder on disk + const formatter = this.formatters.get(workspace.scheme); + const label = options && options.verbose ? this.getUriLabel(workspace) : basenameOrAuthority(workspace); + if (workspace.scheme === Schemas.file) { + return label; + } + + const suffix = formatter && formatter.workspace && (typeof formatter.workspace.suffix === 'string') ? formatter.workspace.suffix : workspace.scheme; + return suffix ? `${label} (${suffix})` : label; + } + + // Workspace: Untitled + if (isParent(workspace.configPath, this.environmentService.workspacesHome, !isLinux /* ignore case */)) { + return localize('untitledWorkspace', "Untitled (Workspace)"); + } + + // Workspace: Saved + const filename = basename(workspace.configPath); + const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + if (options && options.verbose) { + return localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(URI.file(join(dirname(workspace.configPath), workspaceName)))); + } + + return localize('workspaceName', "{0} (Workspace)", workspaceName); + } + + registerFormatter(scheme: string, formatter: LabelRules): IDisposable { + this.formatters.set(scheme, formatter); + this._onDidRegisterFormatter.fire({ scheme, formatter }); + + return { + dispose: () => this.formatters.delete(scheme) + }; + } + + private formatUri(resource: URI, formatter: LabelRules, forceNoTildify: boolean): string { + let label = formatter.uri.label.replace(labelMatchingRegexp, match => { + switch (match) { + case '${scheme}': return resource.scheme; + case '${authority}': return resource.authority; + case '${path}': return resource.path; + default: return ''; + } + }); + + // convert \c:\something => C:\something + if (formatter.uri.normalizeDriveLetter && hasDriveLetter(label)) { + label = label.charAt(1).toUpperCase() + label.substr(2); + } + + if (formatter.uri.tildify && !forceNoTildify) { + label = tildify(label, this.environmentService.userHome); + } + + return label.replace(sepRegexp, formatter.uri.separator); + } +} + +export const ILabelService = createDecorator(LABEL_SERVICE_ID); diff --git a/src/vs/platform/label/electron-browser/label.contribution.ts b/src/vs/platform/label/electron-browser/label.contribution.ts new file mode 100644 index 00000000000..68bd62b54a7 --- /dev/null +++ b/src/vs/platform/label/electron-browser/label.contribution.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// TODO@Isidor bad layering +// tslint:disable-next-line:import-patterns +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ipcRenderer as ipc } from 'electron'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + +/** + * Uri display registration needs to be shared from renderer to main. + * Since there will be another instance of the uri display service running on main. + */ +class LabelRegistrationContribution implements IWorkbenchContribution { + + constructor(@ILabelService labelService: ILabelService) { + labelService.onDidRegisterFormatter(data => { + ipc.send('vscode:labelRegisterFormatter', data); + }); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LabelRegistrationContribution, LifecyclePhase.Starting); diff --git a/src/vs/platform/label/test/label.test.ts b/src/vs/platform/label/test/label.test.ts new file mode 100644 index 00000000000..651638cc176 --- /dev/null +++ b/src/vs/platform/label/test/label.test.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { LabelService } from 'vs/platform/label/common/label'; +import { TestEnvironmentService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { Schemas } from 'vs/base/common/network'; +import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { URI } from 'vs/base/common/uri'; +import { nativeSep } from 'vs/base/common/paths'; +import { isWindows } from 'vs/base/common/platform'; + +suite('URI Label', () => { + + let labelService: LabelService; + + setup(() => { + labelService = new LabelService(TestEnvironmentService, new TestContextService()); + }); + + test('file scheme', function () { + labelService.registerFormatter(Schemas.file, { + uri: { + label: '${path}', + separator: nativeSep, + tildify: !isWindows, + normalizeDriveLetter: isWindows + } + }); + + const uri1 = TestWorkspace.folders[0].uri.with({ path: TestWorkspace.folders[0].uri.path.concat('/a/b/c/d') }); + assert.equal(labelService.getUriLabel(uri1, true), isWindows ? 'a\\b\\c\\d' : 'a/b/c/d'); + assert.equal(labelService.getUriLabel(uri1, false), isWindows ? 'C:\\testWorkspace\\a\\b\\c\\d' : '/testWorkspace/a/b/c/d'); + + const uri2 = URI.file('c:\\1/2/3'); + assert.equal(labelService.getUriLabel(uri2, false), isWindows ? 'C:\\1\\2\\3' : '/c:\\1/2/3'); + }); + + test('custom scheme', function () { + labelService.registerFormatter(Schemas.vscode, { + uri: { + label: 'LABEL/${path}/${authority}/END', + separator: '/', + tildify: true, + normalizeDriveLetter: true + } + }); + + const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); + assert.equal(labelService.getUriLabel(uri1, false), 'LABEL//1/2/3/4/5/microsoft.com/END'); + }); +}); diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts new file mode 100644 index 00000000000..bb3c391ae99 --- /dev/null +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IURLService } from 'vs/platform/url/common/url'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { OpenContext, IWindowSettings } from 'vs/platform/windows/common/windows'; +import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { whenDeleted } from 'vs/base/node/pfs'; +import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { BrowserWindow } from 'electron'; +import { Event } from 'vs/base/common/event'; +import { hasArgs } from 'vs/platform/environment/node/argv'; + +export const ID = 'launchService'; +export const ILaunchService = createDecorator(ID); + +export interface IStartArguments { + args: ParsedArgs; + userEnv: IProcessEnvironment; +} + +export interface IWindowInfo { + pid: number; + title: string; + folderURIs: UriComponents[]; +} + +export interface IMainProcessInfo { + mainPID: number; + mainArguments: string[]; + windows: IWindowInfo[]; +} + +function parseOpenUrl(args: ParsedArgs): URI[] { + if (args['open-url'] && args._urls && args._urls.length > 0) { + // --open-url must contain -- followed by the url(s) + // process.argv is used over args._ as args._ are resolved to file paths at this point + return args._urls + .map(url => { + try { + return URI.parse(url); + } catch (err) { + return null; + } + }) + .filter(uri => !!uri); + } + + return []; +} + +export interface ILaunchService { + _serviceBrand: any; + start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise; + getMainProcessId(): TPromise; + getMainProcessInfo(): TPromise; + getLogsPath(): TPromise; +} + +export interface ILaunchChannel extends IChannel { + call(command: 'start', arg: IStartArguments): TPromise; + call(command: 'get-main-process-id', arg: null): TPromise; + call(command: 'get-main-process-info', arg: null): TPromise; + call(command: 'get-logs-path', arg: null): TPromise; + call(command: string, arg: any): TPromise; +} + +export class LaunchChannel implements ILaunchChannel { + + constructor(private service: ILaunchService) { } + + listen(event: string): Event { + throw new Error('No event found'); + } + + call(command: string, arg: any): TPromise { + switch (command) { + case 'start': + const { args, userEnv } = arg as IStartArguments; + return this.service.start(args, userEnv); + + case 'get-main-process-id': + return this.service.getMainProcessId(); + + case 'get-main-process-info': + return this.service.getMainProcessInfo(); + + case 'get-logs-path': + return this.service.getLogsPath(); + } + + return undefined; + } +} + +export class LaunchChannelClient implements ILaunchService { + + _serviceBrand: any; + + constructor(private channel: ILaunchChannel) { } + + start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { + return this.channel.call('start', { args, userEnv }); + } + + getMainProcessId(): TPromise { + return this.channel.call('get-main-process-id', null); + } + + getMainProcessInfo(): TPromise { + return this.channel.call('get-main-process-info', null); + } + + getLogsPath(): TPromise { + return this.channel.call('get-logs-path', null); + } +} + +export class LaunchService implements ILaunchService { + + _serviceBrand: any; + + constructor( + @ILogService private logService: ILogService, + @IWindowsMainService private windowsMainService: IWindowsMainService, + @IURLService private urlService: IURLService, + @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { } + + start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { + this.logService.trace('Received data from other instance: ', args, userEnv); + + const urlsToOpen = parseOpenUrl(args); + + // Check early for open-url which is handled in URL service + if (urlsToOpen.length) { + let whenWindowReady = TPromise.as(null); + + // Create a window if there is none + if (this.windowsMainService.getWindowCount() === 0) { + const window = this.windowsMainService.openNewWindow(OpenContext.DESKTOP)[0]; + whenWindowReady = window.ready(); + } + + // Make sure a window is open, ready to receive the url event + whenWindowReady.then(() => { + for (const url of urlsToOpen) { + this.urlService.open(url); + } + }); + + return TPromise.as(null); + } + + // Otherwise handle in windows service + return this.startOpenWindow(args, userEnv); + } + + private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { + const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; + let usedWindows: ICodeWindow[]; + + // Special case extension development + if (!!args.extensionDevelopmentPath) { + this.windowsMainService.openExtensionDevelopmentHostWindow({ context, cli: args, userEnv }); + } + + // Start without file/folder arguments + else if (!hasArgs(args._) && !hasArgs(args['folder-uri']) && !hasArgs(args['file-uri'])) { + let openNewWindow = false; + + // Force new window + if (args['new-window'] || args['unity-launch']) { + openNewWindow = true; + } + + // Force reuse window + else if (args['reuse-window']) { + openNewWindow = false; + } + + // Otherwise check for settings + else { + const windowConfig = this.configurationService.getValue('window'); + const openWithoutArgumentsInNewWindowConfig = (windowConfig && windowConfig.openWithoutArgumentsInNewWindow) || 'default' /* default */; + switch (openWithoutArgumentsInNewWindowConfig) { + case 'on': + openNewWindow = true; + break; + case 'off': + openNewWindow = false; + break; + default: + openNewWindow = !isMacintosh; // prefer to restore running instance on macOS + } + } + + if (openNewWindow) { + usedWindows = this.windowsMainService.open({ context, cli: args, userEnv, forceNewWindow: true, forceEmpty: true }); + } else { + usedWindows = [this.windowsMainService.focusLastActive(args, context)]; + } + } + + // Start with file/folder arguments + else { + usedWindows = this.windowsMainService.open({ + context, + cli: args, + userEnv, + forceNewWindow: args['new-window'], + preferNewWindow: !args['reuse-window'] && !args.wait, + forceReuseWindow: args['reuse-window'], + diffMode: args.diff, + addMode: args.add + }); + } + + // If the other instance is waiting to be killed, we hook up a window listener if one window + // is being used and only then resolve the startup promise which will kill this second instance. + // In addition, we poll for the wait marker file to be deleted to return. + if (args.wait && usedWindows.length === 1 && usedWindows[0]) { + return TPromise.any([ + this.windowsMainService.waitForWindowCloseOrLoad(usedWindows[0].id), + whenDeleted(args.waitMarkerFilePath) + ]).then(() => void 0, () => void 0); + } + + return TPromise.as(null); + } + + getMainProcessId(): TPromise { + this.logService.trace('Received request for process ID from other instance.'); + + return TPromise.as(process.pid); + } + + getMainProcessInfo(): TPromise { + this.logService.trace('Received request for main process info from other instance.'); + + const windows: IWindowInfo[] = []; + BrowserWindow.getAllWindows().forEach(window => { + const codeWindow = this.windowsMainService.getWindowById(window.id); + if (codeWindow) { + windows.push(this.codeWindowToInfo(codeWindow)); + } else { + windows.push(this.browserWindowToInfo(window)); + } + }); + + return TPromise.wrap({ + mainPID: process.pid, + mainArguments: process.argv, + windows + } as IMainProcessInfo); + } + + getLogsPath(): TPromise { + this.logService.trace('Received request for logs path from other instance.'); + + return TPromise.as(this.environmentService.logsPath); + } + + private codeWindowToInfo(window: ICodeWindow): IWindowInfo { + const folderURIs: URI[] = []; + + if (window.openedFolderUri) { + folderURIs.push(window.openedFolderUri); + } else if (window.openedWorkspace) { + const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders; + rootFolders.forEach(root => { + folderURIs.push(root.uri); + }); + } + + return this.browserWindowToInfo(window.win, folderURIs); + } + + private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = []): IWindowInfo { + return { + pid: win.webContents.getOSProcessId(), + title: win.getTitle(), + folderURIs + } as IWindowInfo; + } +} diff --git a/src/vs/platform/lifecycle/common/lifecycle.ts b/src/vs/platform/lifecycle/common/lifecycle.ts index 3a92380ea5f..c0a663e258e 100644 --- a/src/vs/platform/lifecycle/common/lifecycle.ts +++ b/src/vs/platform/lifecycle/common/lifecycle.ts @@ -7,6 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { isThenable } from 'vs/base/common/async'; export const ILifecycleService = createDecorator('lifecycleService'); @@ -23,7 +24,7 @@ export interface ShutdownEvent { /** * Allows to veto the shutdown. The veto can be a long running operation. */ - veto(value: boolean | TPromise): void; + veto(value: boolean | Thenable): void; /** * The reason why Code is shutting down. @@ -31,7 +32,7 @@ export interface ShutdownEvent { reason: ShutdownReason; } -export enum ShutdownReason { +export const enum ShutdownReason { /** Window is closed */ CLOSE = 1, @@ -46,18 +47,33 @@ export enum ShutdownReason { LOAD = 4 } -export enum StartupKind { +export const enum StartupKind { NewWindow = 1, ReloadedWindow = 3, ReopenedWindow = 4, } +export function StartupKindToString(startupKind: StartupKind): string { + switch (startupKind) { + case StartupKind.NewWindow: return 'NewWindow'; + case StartupKind.ReloadedWindow: return 'ReloadedWindow'; + case StartupKind.ReopenedWindow: return 'ReopenedWindow'; + } +} -export enum LifecyclePhase { +export const enum LifecyclePhase { Starting = 1, Restoring = 2, Running = 3, Eventually = 4 } +export function LifecyclePhaseToString(phase: LifecyclePhase) { + switch (phase) { + case LifecyclePhase.Starting: return 'Starting'; + case LifecyclePhase.Restoring: return 'Restoring'; + case LifecyclePhase.Running: return 'Running'; + case LifecyclePhase.Eventually: return 'Eventually'; + } +} /** * A lifecycle service informs about lifecycle events of the @@ -108,12 +124,12 @@ export const NullLifecycleService: ILifecycleService = { }; // Shared veto handling across main and renderer -export function handleVetos(vetos: (boolean | TPromise)[], onError: (error: Error) => void): TPromise { +export function handleVetos(vetos: (boolean | Thenable)[], onError: (error: Error) => void): TPromise { if (vetos.length === 0) { return TPromise.as(false); } - const promises: TPromise[] = []; + const promises: Thenable[] = []; let lazyValue = false; for (let valueOrPromise of vetos) { @@ -123,7 +139,7 @@ export function handleVetos(vetos: (boolean | TPromise)[], onError: (er return TPromise.as(true); } - if (TPromise.is(valueOrPromise)) { + if (isThenable(valueOrPromise)) { promises.push(valueOrPromise.then(value => { if (value) { lazyValue = true; // veto, done diff --git a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts index 4b6efd06abb..d75d974b761 100644 --- a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts +++ b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, handleVetos, LifecyclePhaseToString } from 'vs/platform/lifecycle/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ipcRenderer as ipc } from 'electron'; import { Event, Emitter } from 'vs/base/common/event'; @@ -20,7 +20,7 @@ export class LifecycleService implements ILifecycleService { private static readonly _lastShutdownReasonKey = 'lifecyle.lastShutdownReason'; - public _serviceBrand: any; + _serviceBrand: any; private readonly _onWillShutdown = new Emitter(); private readonly _onShutdown = new Emitter(); @@ -61,7 +61,7 @@ export class LifecycleService implements ILifecycleService { this._storageService.store(LifecycleService._lastShutdownReasonKey, JSON.stringify(reply.reason), StorageScope.WORKSPACE); // trigger onWillShutdown events and veto collecting - this.onBeforeUnload(reply.reason).done(veto => { + this.onBeforeUnload(reply.reason).then(veto => { if (veto) { this._logService.trace('lifecycle: onBeforeUnload prevented via veto'); this._storageService.remove(LifecycleService._lastShutdownReasonKey, StorageScope.WORKSPACE); @@ -83,7 +83,7 @@ export class LifecycleService implements ILifecycleService { } private onBeforeUnload(reason: ShutdownReason): TPromise { - const vetos: (boolean | TPromise)[] = []; + const vetos: (boolean | Thenable)[] = []; this._onWillShutdown.fire({ veto(value) { @@ -95,11 +95,11 @@ export class LifecycleService implements ILifecycleService { return handleVetos(vetos, err => this._notificationService.error(toErrorMessage(err))); } - public get phase(): LifecyclePhase { + get phase(): LifecyclePhase { return this._phase; } - public set phase(value: LifecyclePhase) { + set phase(value: LifecyclePhase) { if (value < this.phase) { throw new Error('Lifecycle cannot go backwards'); } @@ -111,7 +111,7 @@ export class LifecycleService implements ILifecycleService { this._logService.trace(`lifecycle: phase changed (value: ${value})`); this._phase = value; - mark(`LifecyclePhase/${LifecyclePhase[value]}`); + mark(`LifecyclePhase/${LifecyclePhaseToString(value)}`); if (this._phaseWhen.has(this._phase)) { this._phaseWhen.get(this._phase).open(); @@ -119,7 +119,7 @@ export class LifecycleService implements ILifecycleService { } } - public when(phase: LifecyclePhase): Thenable { + when(phase: LifecyclePhase): Thenable { if (phase <= this._phase) { return Promise.resolve(); } @@ -133,15 +133,15 @@ export class LifecycleService implements ILifecycleService { return barrier.wait(); } - public get startupKind(): StartupKind { + get startupKind(): StartupKind { return this._startupKind; } - public get onWillShutdown(): Event { + get onWillShutdown(): Event { return this._onWillShutdown.event; } - public get onShutdown(): Event { + get onShutdown(): Event { return this._onShutdown.event; } } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index a2388d8fa49..08641ed453d 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -18,7 +18,7 @@ import { isMacintosh, isWindows } from 'vs/base/common/platform'; export const ILifecycleService = createDecorator('lifecycleService'); -export enum UnloadReason { +export const enum UnloadReason { CLOSE = 1, QUIT = 2, RELOAD = 3, @@ -129,15 +129,15 @@ export class LifecycleService implements ILifecycleService { } } - public get wasRestarted(): boolean { + get wasRestarted(): boolean { return this._wasRestarted; } - public get isQuitRequested(): boolean { + get isQuitRequested(): boolean { return !!this.quitRequested; } - public ready(): void { + ready(): void { this.registerListeners(); } @@ -178,7 +178,7 @@ export class LifecycleService implements ILifecycleService { }); } - public registerWindow(window: ICodeWindow): void { + registerWindow(window: ICodeWindow): void { // track window count this.windowCounter++; @@ -199,7 +199,7 @@ export class LifecycleService implements ILifecycleService { // Otherwise prevent unload and handle it from window e.preventDefault(); - this.unload(window, UnloadReason.CLOSE).done(veto => { + this.unload(window, UnloadReason.CLOSE).then(veto => { if (!veto) { this.windowToCloseRequest[windowId] = true; @@ -232,7 +232,7 @@ export class LifecycleService implements ILifecycleService { }); } - public unload(window: ICodeWindow, reason: UnloadReason): TPromise { + unload(window: ICodeWindow, reason: UnloadReason): TPromise { // Always allow to unload a window that is not yet ready if (window.readyState !== ReadyState.READY) { @@ -326,7 +326,7 @@ export class LifecycleService implements ILifecycleService { * A promise that completes to indicate if the quit request has been veto'd * by the user or not. */ - public quit(fromUpdate?: boolean): TPromise { + quit(fromUpdate?: boolean): TPromise { this.logService.trace('Lifecycle#quit()'); if (!this.pendingQuitPromise) { @@ -362,13 +362,13 @@ export class LifecycleService implements ILifecycleService { return this.pendingQuitPromise; } - public kill(code?: number): void { + kill(code?: number): void { this.logService.trace('Lifecycle#kill()'); app.exit(code); } - public relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void { + relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void { this.logService.trace('Lifecycle#relaunch()'); const args = process.argv.slice(1); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index bc246f6ed0c..627ad10eeff 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -4,27 +4,35 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; -import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController, IOpenController, DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, toDisposable, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; -import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list'; +import { addClass, addStandardDisposableListener, createStyleSheet, getTotalHeight, removeClass } from 'vs/base/browser/dom'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IListMouseEvent, IListTouchEvent, IRenderer, IVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IPagedRenderer, PagedList } from 'vs/base/browser/ui/list/listPaging'; +import { DefaultStyleController, IListOptions, IMultipleSelectionController, IOpenController, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, List } from 'vs/base/browser/ui/list/listWidget'; +import { canceled, onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IFilter, ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; +import { ClickBehavior, DefaultController, DefaultTreestyler, IControllerOptions, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; -import { attachListStyler, defaultListStyles, computeStyles } from 'vs/platform/theme/common/styler'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { attachInputBoxStyler, attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { localize } from 'vs/nls'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { DefaultController, IControllerOptions, OpenMode, ClickBehavior, DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults'; -import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { Event, Emitter } from 'vs/base/common/event'; -import { createStyleSheet } from 'vs/base/browser/dom'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export type ListWidget = List | PagedList | ITree; @@ -90,16 +98,12 @@ export class ListService implements IListService { const RawWorkbenchListFocusContextKey = new RawContextKey('listFocus', true); export const WorkbenchListSupportsMultiSelectContextKey = new RawContextKey('listSupportsMultiselect', true); export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey)); +export const WorkbenchListHasSelectionOrFocus = new RawContextKey('listHasSelectionOrFocus', false); export const WorkbenchListDoubleSelection = new RawContextKey('listDoubleSelection', false); export const WorkbenchListMultiSelection = new RawContextKey('listMultiSelection', false); function createScopedContextKeyService(contextKeyService: IContextKeyService, widget: ListWidget): IContextKeyService { const result = contextKeyService.createScoped(widget.getHTMLElement()); - - if (widget instanceof List || widget instanceof PagedList) { - WorkbenchListSupportsMultiSelectContextKey.bindTo(result); - } - RawWorkbenchListFocusContextKey.bindTo(result); return result; } @@ -199,6 +203,7 @@ export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -206,7 +211,7 @@ export class WorkbenchList extends List { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -225,6 +230,11 @@ export class WorkbenchList extends List { ); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -236,8 +246,17 @@ export class WorkbenchList extends List { attachListStyler(this, themeService), this.onSelectionChange(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); this.listMultiSelection.set(selection.length > 1); this.listDoubleSelection.set(selection.length === 2); + }), + this.onFocusChange(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); }) ])); @@ -267,7 +286,7 @@ export class WorkbenchPagedList extends PagedList { constructor( container: HTMLElement, - delegate: IDelegate, + delegate: IVirtualDelegate, renderers: IPagedRenderer[], options: IListOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -287,6 +306,9 @@ export class WorkbenchPagedList extends PagedList { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); this.disposables.push(combinedDisposable([ @@ -323,6 +345,7 @@ export class WorkbenchTree extends Tree { protected disposables: IDisposable[]; + private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -352,6 +375,10 @@ export class WorkbenchTree extends Tree { this.disposables = []; this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + + WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -366,10 +393,20 @@ export class WorkbenchTree extends Tree { this.disposables.push(this.onDidChangeSelection(() => { const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); this.listDoubleSelection.set(selection && selection.length === 2); this.listMultiSelection.set(selection && selection.length > 1); })); + this.disposables.push(this.onDidChangeFocus(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus); + })); + this.disposables.push(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(openModeSettingKey)) { this._openOnSingleClick = useSingleClickToOpen(configurationService); @@ -526,6 +563,241 @@ export class TreeResourceNavigator extends Disposable { } } +export interface IHighlighter { + getHighlights(tree: ITree, element: any, pattern: string): FuzzyScore; + getHighlightsStorageKey?(element: any): any; +} + +export interface IHighlightingTreeConfiguration extends ITreeConfiguration { + highlighter: IHighlighter; +} + +export interface IHighlightingTreeOptions extends ITreeOptions { + filterOnType?: boolean; +} + +export class HighlightingTreeController extends WorkbenchTreeController { + + constructor( + options: IControllerOptions, + private readonly onType: () => any, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(options, configurationService); + } + + onKeyDown(tree: ITree, event: IKeyboardEvent) { + let handled = super.onKeyDown(tree, event); + if (handled) { + return true; + } + if (this.upKeyBindingDispatcher.has(event.keyCode)) { + return false; + } + if (this._keybindingService.mightProducePrintableCharacter(event)) { + this.onType(); + return true; + } + return false; + } +} + +class HightlightsFilter implements IFilter { + + static add(config: ITreeConfiguration, options: IHighlightingTreeOptions): ITreeConfiguration { + let myFilter = new HightlightsFilter(); + myFilter.enabled = options.filterOnType; + if (!config.filter) { + config.filter = myFilter; + } else { + let otherFilter = config.filter; + config.filter = { + isVisible(tree: ITree, element: any): boolean { + return myFilter.isVisible(tree, element) && otherFilter.isVisible(tree, element); + } + }; + } + return config; + } + + enabled: boolean = true; + + isVisible(tree: ITree, element: any): boolean { + if (!this.enabled) { + return true; + } + let tree2 = (tree as HighlightingWorkbenchTree); + if (!tree2.isHighlighterScoring()) { + return true; + } + if (tree2.getHighlighterScore(element)) { + return true; + } + return false; + } +} + +export class HighlightingWorkbenchTree extends WorkbenchTree { + + protected readonly domNode: HTMLElement; + protected readonly inputContainer: HTMLElement; + protected readonly input: InputBox; + + protected readonly highlighter: IHighlighter; + protected readonly highlights: Map; + + private readonly _onDidStartFilter: Emitter; + readonly onDidStartFiltering: Event; + + constructor( + parent: HTMLElement, + treeConfiguration: IHighlightingTreeConfiguration, + treeOptions: IHighlightingTreeOptions, + listOptions: IInputOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextViewService contextViewService: IContextViewService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + // build html skeleton + const container = document.createElement('div'); + container.className = 'highlighting-tree'; + const inputContainer = document.createElement('div'); + inputContainer.className = 'input'; + const treeContainer = document.createElement('div'); + treeContainer.className = 'tree'; + container.appendChild(inputContainer); + container.appendChild(treeContainer); + parent.appendChild(container); + + // create tree + treeConfiguration.controller = treeConfiguration.controller || instantiationService.createInstance(HighlightingTreeController, {}, () => this.onTypeInTree()); + super(treeContainer, HightlightsFilter.add(treeConfiguration, treeOptions), treeOptions, contextKeyService, listService, themeService, instantiationService, configurationService); + this.highlighter = treeConfiguration.highlighter; + this.highlights = new Map(); + + this.domNode = container; + addClass(this.domNode, 'inactive'); + + // create input + this.inputContainer = inputContainer; + this.input = new InputBox(inputContainer, contextViewService, listOptions); + this.input.setEnabled(false); + this.input.onDidChange(this.updateHighlights, this, this.disposables); + this.disposables.push(attachInputBoxStyler(this.input, themeService)); + this.disposables.push(this.input); + this.disposables.push(addStandardDisposableListener(this.input.inputElement, 'keydown', event => { + //todo@joh make this command/context-key based + switch (event.keyCode) { + case KeyCode.UpArrow: + case KeyCode.DownArrow: + case KeyCode.Tab: + this.domFocus(); + event.preventDefault(); + break; + case KeyCode.Enter: + this.setSelection(this.getSelection()); + event.preventDefault(); + break; + case KeyCode.Escape: + this.input.value = ''; + this.domFocus(); + event.preventDefault(); + break; + } + })); + + this._onDidStartFilter = new Emitter(); + this.onDidStartFiltering = this._onDidStartFilter.event; + this.disposables.push(this._onDidStartFilter); + } + + setInput(element: any): TPromise { + this.input.setEnabled(false); + return super.setInput(element).then(value => { + if (!this.input.inputElement) { + // has been disposed in the meantime -> cancel + return Promise.reject(canceled()); + } + this.input.setEnabled(true); + return value; + }); + } + + layout(height?: number, width?: number): void { + this.input.layout(); + super.layout(isNaN(height) ? height : height - getTotalHeight(this.inputContainer), width); + } + + private onTypeInTree(): void { + removeClass(this.domNode, 'inactive'); + this.input.focus(); + this.layout(); + this._onDidStartFilter.fire(this); + } + + private lastSelection: any[]; + + private updateHighlights(pattern: string): void { + + // remember old selection + let defaultSelection: any[] = []; + if (!this.lastSelection && pattern) { + this.lastSelection = this.getSelection(); + } else if (this.lastSelection && !pattern) { + defaultSelection = this.lastSelection; + this.lastSelection = []; + } + + let topElement: any; + if (pattern) { + let nav = this.getNavigator(undefined, false); + let topScore: FuzzyScore; + while (nav.next()) { + let element = nav.current(); + let score = this.highlighter.getHighlights(this, element, pattern); + this.highlights.set(this._getHighlightsStorageKey(element), score); + element.foo = 1; + if (!topScore || score && topScore[0] < score[0]) { + topScore = score; + topElement = element; + } + } + } else { + // no pattern, clear highlights + this.highlights.clear(); + } + + this.refresh().then(() => { + if (topElement) { + this.reveal(topElement, .5).then(_ => { + this.setSelection([topElement], this); + this.setFocus(topElement, this); + }); + } else { + this.setSelection(defaultSelection, this); + } + }, onUnexpectedError); + } + + isHighlighterScoring(): boolean { + return this.highlights.size > 0; + } + + getHighlighterScore(element: any): FuzzyScore { + return this.highlights.get(this._getHighlightsStorageKey(element)); + } + + private _getHighlightsStorageKey(element: any): any { + return typeof this.highlighter.getHighlightsStorageKey === 'function' + ? this.highlighter.getHighlightsStorageKey(element) + : element; + } +} + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ @@ -548,20 +820,16 @@ configurationRegistry.registerConfiguration({ '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") + }, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.") }, [openModeSettingKey]: { 'type': 'string', 'enum': ['singleClick', 'doubleClick'], - 'enumDescriptions': [ - localize('openMode.singleClick', "Opens items on mouse single click."), - localize('openMode.doubleClick', "Open items on mouse double click.") - ], 'default': 'singleClick', 'description': localize({ key: 'openModeModifier', comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] - }, "Controls how to open items in trees and lists using the mouse (if supported). Set to `singleClick` to open items with a single mouse click and `doubleClick` to only open via mouse double click. For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ") + }, "Controls how to open items in trees and lists using the mouse (if supported). For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ") }, [horizontalScrollingKey]: { 'type': 'boolean', diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index 1f85c99a746..6aec8411f31 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -21,7 +21,7 @@ export interface ITranslation { path: string; } -export enum LanguageType { +export const enum LanguageType { Core = 1, Contributed } diff --git a/src/vs/platform/localizations/common/localizationsIpc.ts b/src/vs/platform/localizations/common/localizationsIpc.ts deleted file mode 100644 index 51b6fb34147..00000000000 --- a/src/vs/platform/localizations/common/localizationsIpc.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; -import { Event, buffer } from 'vs/base/common/event'; -import { ILocalizationsService, LanguageType } from 'vs/platform/localizations/common/localizations'; - -export interface ILocalizationsChannel extends IChannel { - call(command: 'event:onDidLanguagesChange'): TPromise; - call(command: 'getLanguageIds'): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class LocalizationsChannel implements ILocalizationsChannel { - - onDidLanguagesChange: Event; - - constructor(private service: ILocalizationsService) { - this.onDidLanguagesChange = buffer(service.onDidLanguagesChange, true); - } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'event:onDidLanguagesChange': return eventToCall(this.onDidLanguagesChange); - case 'getLanguageIds': return this.service.getLanguageIds(arg); - } - return undefined; - } -} - -export class LocalizationsChannelClient implements ILocalizationsService { - - _serviceBrand: any; - - constructor(private channel: ILocalizationsChannel) { } - - private _onDidLanguagesChange = eventFromCall(this.channel, 'event:onDidLanguagesChange'); - get onDidLanguagesChange(): Event { return this._onDidLanguagesChange; } - - getLanguageIds(type?: LanguageType): TPromise { - return this.channel.call('getLanguageIds', type); - } -} \ No newline at end of file diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index b736df9096d..46bd114b4cf 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -8,7 +8,6 @@ import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { join } from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import { Limiter } from 'vs/base/common/async'; import { areSameExtensions, getGalleryExtensionIdFromLocal, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -17,6 +16,8 @@ import { isValidLocalization, ILocalizationsService, LanguageType } from 'vs/pla import product from 'vs/platform/node/product'; import { distinct, equals } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; +import { posix } from 'path'; interface ILanguagePack { hash: string; @@ -27,7 +28,7 @@ interface ILanguagePack { translations: { [id: string]: string }; } -const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-Hans', 'zh-TW', 'zh-Hant']; +const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-TW']; if (product.quality !== 'stable') { systemLanguages.push('hu'); } @@ -106,7 +107,7 @@ class LanguagePacksCache extends Disposable { @ILogService private logService: ILogService ) { super(); - this.languagePacksFilePath = join(environmentService.userDataPath, 'languagepacks.json'); + this.languagePacksFilePath = posix.join(environmentService.userDataPath, 'languagepacks.json'); this.languagePacksFileLimiter = new Limiter(1); } @@ -138,7 +139,7 @@ class LanguagePacksCache extends Disposable { private createLanguagePacksFromExtension(languagePacks: { [language: string]: ILanguagePack }, extension: ILocalExtension): void { const extensionIdentifier = { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; for (const localizationContribution of extension.manifest.contributes.localizations) { - if (isValidLocalization(localizationContribution)) { + if (extension.location.scheme === Schemas.file && isValidLocalization(localizationContribution)) { let languagePack = languagePacks[localizationContribution.languageId]; if (!languagePack) { languagePack = { hash: '', extensions: [], translations: {} }; @@ -151,7 +152,7 @@ class LanguagePacksCache extends Disposable { languagePack.extensions.push({ extensionIdentifier, version: extension.manifest.version }); } for (const translation of localizationContribution.translations) { - languagePack.translations[translation.id] = join(extension.path, translation.path); + languagePack.translations[translation.id] = posix.join(extension.location.fsPath, translation.path); } } } diff --git a/src/vs/platform/localizations/node/localizationsIpc.ts b/src/vs/platform/localizations/node/localizationsIpc.ts new file mode 100644 index 00000000000..e58a97bd985 --- /dev/null +++ b/src/vs/platform/localizations/node/localizationsIpc.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { Event, buffer } from 'vs/base/common/event'; +import { ILocalizationsService, LanguageType } from 'vs/platform/localizations/common/localizations'; + +export interface ILocalizationsChannel extends IChannel { + listen(event: 'onDidLanguagesChange'): Event; + listen(event: string, arg?: any): Event; + + call(command: 'getLanguageIds'): Thenable; + call(command: string, arg?: any): Thenable; +} + +export class LocalizationsChannel implements ILocalizationsChannel { + + onDidLanguagesChange: Event; + + constructor(private service: ILocalizationsService) { + this.onDidLanguagesChange = buffer(service.onDidLanguagesChange, true); + } + + listen(event: string): Event { + switch (event) { + case 'onDidLanguagesChange': return this.onDidLanguagesChange; + } + + throw new Error('No event found'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'getLanguageIds': return this.service.getLanguageIds(arg); + } + return undefined; + } +} + +export class LocalizationsChannelClient implements ILocalizationsService { + + _serviceBrand: any; + + constructor(private channel: ILocalizationsChannel) { } + + get onDidLanguagesChange(): Event { return this.channel.listen('onDidLanguagesChange'); } + + getLanguageIds(type?: LanguageType): TPromise { + return TPromise.wrap(this.channel.call('getLanguageIds', type)); + } +} \ No newline at end of file diff --git a/src/vs/platform/log/common/bufferLog.ts b/src/vs/platform/log/common/bufferLog.ts index 65fa32adb0e..437fd54d2f5 100644 --- a/src/vs/platform/log/common/bufferLog.ts +++ b/src/vs/platform/log/common/bufferLog.ts @@ -30,6 +30,15 @@ export class BufferLogService extends AbstractLogService implements ILogService private buffer: ILog[] = []; private _logger: ILogService | undefined = undefined; + constructor() { + super(); + this._register(this.onDidChangeLogLevel(level => { + if (this._logger) { + this._logger.setLevel(level); + } + })); + } + set logger(logger: ILogService) { this._logger = logger; diff --git a/src/vs/platform/log/common/logIpc.ts b/src/vs/platform/log/node/logIpc.ts similarity index 77% rename from src/vs/platform/log/common/logIpc.ts rename to src/vs/platform/log/node/logIpc.ts index d39072ea674..ca2cb87d67c 100644 --- a/src/vs/platform/log/common/logIpc.ts +++ b/src/vs/platform/log/node/logIpc.ts @@ -3,14 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; import { TPromise } from 'vs/base/common/winjs.base'; import { LogLevel, ILogService, DelegatedLogService } from 'vs/platform/log/common/log'; import { Event, buffer } from 'vs/base/common/event'; export interface ILogLevelSetterChannel extends IChannel { - call(command: 'event:onDidChangeLogLevel'): TPromise; + listen(event: 'onDidChangeLogLevel'): Event; + listen(event: string, arg?: any): Event; + call(command: 'setLevel', logLevel: LogLevel): TPromise; + call(command: string, arg?: any): TPromise; } export class LogLevelSetterChannel implements ILogLevelSetterChannel { @@ -21,9 +24,16 @@ export class LogLevelSetterChannel implements ILogLevelSetterChannel { this.onDidChangeLogLevel = buffer(service.onDidChangeLogLevel, true); } + listen(event: string): Event { + switch (event) { + case 'onDidChangeLogLevel': return this.onDidChangeLogLevel; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onDidChangeLogLevel': return eventToCall(this.onDidChangeLogLevel); case 'setLevel': this.service.setLevel(arg); return TPromise.as(null); } return undefined; @@ -34,8 +44,9 @@ export class LogLevelSetterChannelClient { constructor(private channel: ILogLevelSetterChannel) { } - private _onDidChangeLogLevel = eventFromCall(this.channel, 'event:onDidChangeLogLevel'); - get onDidChangeLogLevel(): Event { return this._onDidChangeLogLevel; } + get onDidChangeLogLevel(): Event { + return this.channel.listen('onDidChangeLogLevel'); + } setLevel(level: LogLevel): TPromise { return this.channel.call('setLevel', level); diff --git a/src/vs/platform/log/node/spdlogService.ts b/src/vs/platform/log/node/spdlogService.ts index 5d579767204..d2bd5c19658 100644 --- a/src/vs/platform/log/node/spdlogService.ts +++ b/src/vs/platform/log/node/spdlogService.ts @@ -13,7 +13,7 @@ export function createSpdLogService(processName: string, logLevel: LogLevel, log // Do not crash if spdlog cannot be loaded try { const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); - _spdlog.setAsyncMode(8192, 2000); + _spdlog.setAsyncMode(8192, 500); const logfilePath = path.join(logsFolder, `${processName}.log`); const logger = new _spdlog.RotatingLogger(processName, logfilePath, 1024 * 1024 * 5, 6); logger.setLevel(0); @@ -25,6 +25,11 @@ export function createSpdLogService(processName: string, logLevel: LogLevel, log return new NullLogService(); } +export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger { + const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + return new _spdlog.RotatingLogger(name, filename, filesize, filecount); +} + class SpdLogService extends AbstractLogService implements ILogService { _serviceBrand: any; diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 8329796d9b6..e57da274003 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -8,7 +8,7 @@ import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { Schemas } from 'vs/base/common/network'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isEmptyObject } from 'vs/base/common/types'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter, debounceEvent } from 'vs/base/common/event'; import { IMarkerService, IMarkerData, IResourceMarker, IMarker, MarkerStatistics, MarkerSeverity } from './markers'; @@ -184,7 +184,7 @@ export class MarkerService implements IMarkerService { message, source, startLineNumber, startColumn, endLineNumber, endColumn, relatedInformation, - customTags, + tags, } = data; if (!message) { @@ -210,7 +210,7 @@ export class MarkerService implements IMarkerService { endLineNumber, endColumn, relatedInformation, - customTags, + tags, }; } diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 69f34f9400f..59bd04c9735 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; @@ -87,7 +87,7 @@ export interface IMarkerData { endLineNumber: number; endColumn: number; relatedInformation?: IRelatedInformation[]; - customTags?: MarkerTag[]; + tags?: MarkerTag[]; } export interface IResourceMarker { @@ -107,7 +107,7 @@ export interface IMarker { endLineNumber: number; endColumn: number; relatedInformation?: IRelatedInformation[]; - customTags?: MarkerTag[]; + tags?: MarkerTag[]; } export interface MarkerStatistics { diff --git a/src/vs/platform/markers/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index 06495c239e4..e61abb8ec3e 100644 --- a/src/vs/platform/markers/test/common/markerService.test.ts +++ b/src/vs/platform/markers/test/common/markerService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as markerService from 'vs/platform/markers/common/markerService'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -58,16 +58,16 @@ suite('Marker Service', () => { test('changeOne override', () => { let service = new markerService.MarkerService(); - service.changeOne('far', URI.parse('/path/only.cs'), [randomMarkerData()]); + service.changeOne('far', URI.parse('file:///path/only.cs'), [randomMarkerData()]); assert.equal(service.read().length, 1); assert.equal(service.read({ owner: 'far' }).length, 1); - service.changeOne('boo', URI.parse('/path/only.cs'), [randomMarkerData()]); + service.changeOne('boo', URI.parse('file:///path/only.cs'), [randomMarkerData()]); assert.equal(service.read().length, 2); assert.equal(service.read({ owner: 'far' }).length, 1); assert.equal(service.read({ owner: 'boo' }).length, 1); - service.changeOne('far', URI.parse('/path/only.cs'), [randomMarkerData(), randomMarkerData()]); + service.changeOne('far', URI.parse('file:///path/only.cs'), [randomMarkerData(), randomMarkerData()]); assert.equal(service.read({ owner: 'far' }).length, 2); assert.equal(service.read({ owner: 'boo' }).length, 1); @@ -76,13 +76,13 @@ suite('Marker Service', () => { test('changeOne/All clears', () => { let service = new markerService.MarkerService(); - service.changeOne('far', URI.parse('/path/only.cs'), [randomMarkerData()]); - service.changeOne('boo', URI.parse('/path/only.cs'), [randomMarkerData()]); + service.changeOne('far', URI.parse('file:///path/only.cs'), [randomMarkerData()]); + service.changeOne('boo', URI.parse('file:///path/only.cs'), [randomMarkerData()]); assert.equal(service.read({ owner: 'far' }).length, 1); assert.equal(service.read({ owner: 'boo' }).length, 1); assert.equal(service.read().length, 2); - service.changeOne('far', URI.parse('/path/only.cs'), []); + service.changeOne('far', URI.parse('file:///path/only.cs'), []); assert.equal(service.read({ owner: 'far' }).length, 0); assert.equal(service.read({ owner: 'boo' }).length, 1); assert.equal(service.read().length, 1); diff --git a/src/vs/platform/menubar/common/menubar.ts b/src/vs/platform/menubar/common/menubar.ts new file mode 100644 index 00000000000..3e58c3e4c9b --- /dev/null +++ b/src/vs/platform/menubar/common/menubar.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IMenubarService = createDecorator('menubarService'); + +export interface IMenubarService { + _serviceBrand: any; + + updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array): TPromise; +} + +export interface IMenubarData { + 'Files'?: IMenubarMenu; + 'Edit'?: IMenubarMenu; + [id: string]: IMenubarMenu; +} + +export interface IMenubarMenu { + items: Array; +} + +export interface IMenubarKeybinding { + id: string; + label: string; + isNative: boolean; +} + +export interface IMenubarMenuItemAction { + id: string; + label: string; + checked: boolean; + enabled: boolean; + keybinding?: IMenubarKeybinding; +} + +export interface IMenubarMenuItemSubmenu { + id: string; + label: string; + submenu: IMenubarMenu; +} + +export interface IMenubarMenuItemSeparator { + id: 'vscode.menubar.separator'; +} + +export type MenubarMenuItem = IMenubarMenuItemAction | IMenubarMenuItemSubmenu | IMenubarMenuItemSeparator; + +export function isMenubarMenuItemSubmenu(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemSubmenu { + return (menuItem).submenu !== undefined; +} + +export function isMenubarMenuItemAction(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemAction { + return (menuItem).checked !== undefined || (menuItem).enabled !== undefined; +} + +export function isMenubarMenuItemSeparator(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemSeparator { + return (menuItem).id === 'vscode.menubar.separator'; +} \ No newline at end of file diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts new file mode 100644 index 00000000000..3113dadb3f6 --- /dev/null +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -0,0 +1,789 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vs/nls'; +import { isMacintosh, language } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron'; +import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import product from 'vs/platform/node/product'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel } from 'vs/base/common/labels'; +import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IHistoryMainService } from 'vs/platform/history/common/history'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction } from 'vs/platform/menubar/common/menubar'; +import { URI } from 'vs/base/common/uri'; +import { ILabelService } from 'vs/platform/label/common/label'; + +const telemetryFrom = 'menu'; + +export class Menubar { + + private static readonly MAX_MENU_RECENT_ENTRIES = 10; + private isQuitting: boolean; + private appMenuInstalled: boolean; + private closedLastWindow: boolean; + + private menuUpdater: RunOnceScheduler; + + private menubarMenus: IMenubarData; + + private keybindings: { [commandId: string]: IMenubarKeybinding }; + + constructor( + @IUpdateService private updateService: IUpdateService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService, + @IWindowsMainService private windowsMainService: IWindowsMainService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ITelemetryService private telemetryService: ITelemetryService, + @IHistoryMainService private historyMainService: IHistoryMainService, + @ILabelService private labelService: ILabelService + ) { + this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); + + this.keybindings = Object.create(null); + + this.closedLastWindow = false; + + this.install(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Keep flag when app quits + app.on('will-quit', () => { + this.isQuitting = true; + }); + + // // Listen to some events from window service to update menu + this.historyMainService.onRecentlyOpenedChange(() => this.scheduleUpdateMenu()); + this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e)); + // this.windowsMainService.onActiveWindowChanged(() => this.updateWorkspaceMenuItems()); + // this.windowsMainService.onWindowReady(() => this.updateWorkspaceMenuItems()); + // this.windowsMainService.onWindowClose(() => this.updateWorkspaceMenuItems()); + + // Update when auto save config changes + // this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)); + + // Listen to update service + // this.updateService.onStateChange(() => this.updateMenu()); + } + + private get currentEnableMenuBarMnemonics(): boolean { + let enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics'); + if (typeof enableMenuBarMnemonics !== 'boolean') { + enableMenuBarMnemonics = true; + } + + return enableMenuBarMnemonics; + } + + private get currentEnableNativeTabs(): boolean { + let enableNativeTabs = this.configurationService.getValue('window.nativeTabs'); + if (typeof enableNativeTabs !== 'boolean') { + enableNativeTabs = false; + } + return enableNativeTabs; + } + + updateMenu(menus: IMenubarData, windowId: number, additionalKeybindings?: Array) { + this.menubarMenus = menus; + if (additionalKeybindings) { + additionalKeybindings.forEach(keybinding => { + this.keybindings[keybinding.id] = keybinding; + }); + } + + this.scheduleUpdateMenu(); + } + + + private scheduleUpdateMenu(): void { + this.menuUpdater.schedule(); // buffer multiple attempts to update the menu + } + + private doUpdateMenu(): void { + + // Due to limitations in Electron, it is not possible to update menu items dynamically. The suggested + // workaround from Electron is to set the application menu again. + // See also https://github.com/electron/electron/issues/846 + // + // Run delayed to prevent updating menu while it is open + if (!this.isQuitting) { + setTimeout(() => { + if (!this.isQuitting) { + this.install(); + } + }, 10 /* delay this because there is an issue with updating a menu when it is open */); + } + } + + private onWindowsCountChanged(e: IWindowsCountChangedEvent): void { + if (!isMacintosh) { + return; + } + + // Update menu if window count goes from N > 0 or 0 > N to update menu item enablement + if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) { + this.closedLastWindow = e.newCount === 0; + this.scheduleUpdateMenu(); + } + } + + private install(): void { + + // Menus + const menubar = new Menu(); + + // Mac: Application + let macApplicationMenuItem: Electron.MenuItem; + if (isMacintosh) { + const applicationMenu = new Menu(); + macApplicationMenuItem = new MenuItem({ label: product.nameShort, submenu: applicationMenu }); + this.setMacApplicationMenu(applicationMenu); + menubar.append(macApplicationMenuItem); + } + + // Mac: Dock + if (isMacintosh && !this.appMenuInstalled) { + this.appMenuInstalled = true; + + const dockMenu = new Menu(); + dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.DOCK) })); + + app.dock.setMenu(dockMenu); + } + + // File + const fileMenu = new Menu(); + const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); + + if (this.shouldDrawMenu('File')) { + if (this.shouldFallback('File')) { + this.setFallbackMenuById(fileMenu, 'File'); + } else { + this.setMenuById(fileMenu, 'File'); + } + + menubar.append(fileMenuItem); + } + + + // Edit + const editMenu = new Menu(); + const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); + + if (this.shouldDrawMenu('Edit')) { + this.setMenuById(editMenu, 'Edit'); + menubar.append(editMenuItem); + } + + // Selection + const selectionMenu = new Menu(); + const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); + + if (this.shouldDrawMenu('Selection')) { + this.setMenuById(selectionMenu, 'Selection'); + menubar.append(selectionMenuItem); + } + + // View + const viewMenu = new Menu(); + const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); + + if (this.shouldDrawMenu('View')) { + this.setMenuById(viewMenu, 'View'); + menubar.append(viewMenuItem); + } + + // Layout + const layoutMenu = new Menu(); + const layoutMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mLayout', comment: ['&& denotes a mnemonic'] }, "&&Layout")), submenu: layoutMenu }); + + if (this.shouldDrawMenu('Layout')) { + this.setMenuById(layoutMenu, 'Layout'); + menubar.append(layoutMenuItem); + } + + // Go + const gotoMenu = new Menu(); + const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); + + if (this.shouldDrawMenu('Go')) { + this.setMenuById(gotoMenu, 'Go'); + menubar.append(gotoMenuItem); + } + + // Terminal + const terminalMenu = new Menu(); + const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu }); + + if (this.shouldDrawMenu('Terminal')) { + this.setMenuById(terminalMenu, 'Terminal'); + menubar.append(terminalMenuItem); + } + + // Debug + const debugMenu = new Menu(); + const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu }); + + if (this.shouldDrawMenu('Debug')) { + this.setMenuById(debugMenu, 'Debug'); + menubar.append(debugMenuItem); + } + + // Tasks + const taskMenu = new Menu(); + const taskMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTask', comment: ['&& denotes a mnemonic'] }, "&&Tasks")), submenu: taskMenu }); + + if (this.shouldDrawMenu('Tasks')) { + this.setMenuById(taskMenu, 'Tasks'); + menubar.append(taskMenuItem); + } + + // Mac: Window + let macWindowMenuItem: Electron.MenuItem; + if (this.shouldDrawMenu('Window')) { + const windowMenu = new Menu(); + macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' }); + this.setMacWindowMenu(windowMenu); + } + + if (macWindowMenuItem) { + menubar.append(macWindowMenuItem); + } + + // Preferences + if (!isMacintosh) { + const preferencesMenu = new Menu(); + const preferencesMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu }); + + if (this.shouldDrawMenu('Preferences')) { + if (this.shouldFallback('Preferences')) { + this.setFallbackMenuById(preferencesMenu, 'Preferences'); + } else { + this.setMenuById(preferencesMenu, 'Preferences'); + } + menubar.append(preferencesMenuItem); + } + } + + // Help + const helpMenu = new Menu(); + const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' }); + + if (this.shouldDrawMenu('Help')) { + if (this.shouldFallback('Help')) { + this.setFallbackMenuById(helpMenu, 'Help'); + } else { + this.setMenuById(helpMenu, 'Help'); + } + menubar.append(helpMenuItem); + } + + if (menubar.items && menubar.items.length > 0) { + Menu.setApplicationMenu(menubar); + } else { + Menu.setApplicationMenu(null); + } + } + + private setMacApplicationMenu(macApplicationMenu: Electron.Menu): void { + const about = new MenuItem({ label: nls.localize('mAbout', "About {0}", product.nameLong), role: 'about' }); + const checkForUpdates = this.getUpdateMenuItems(); + + let preferences; + if (this.shouldDrawMenu('Preferences')) { + const preferencesMenu = new Menu(); + this.setMenuById(preferencesMenu, 'Preferences'); + preferences = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu }); + } + + const servicesMenu = new Menu(); + const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu }); + const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", product.nameLong), role: 'hide', accelerator: 'Command+H' }); + const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideothers', accelerator: 'Command+Alt+H' }); + const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' }); + const quit = new MenuItem(this.likeAction('workbench.action.quit', { + label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => { + if (this.windowsMainService.getWindowCount() === 0 || !!BrowserWindow.getFocusedWindow()) { + this.windowsMainService.quit(); // fix for https://github.com/Microsoft/vscode/issues/39191 + } + } + })); + + const actions = [about]; + actions.push(...checkForUpdates); + + if (preferences) { + actions.push(...[ + __separator__(), + preferences + ]); + } + + actions.push(...[ + __separator__(), + services, + __separator__(), + hide, + hideOthers, + showAll, + __separator__(), + quit + ]); + + actions.forEach(i => macApplicationMenu.append(i)); + } + + private shouldDrawMenu(menuId: string): boolean { + // We need to draw an empty menu to override the electron default + if (!isMacintosh && this.configurationService.getValue('window.titleBarStyle') === 'custom') { + return false; + } + + switch (menuId) { + case 'File': + case 'Help': + if (isMacintosh) { + return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]); + } + + case 'Window': + if (isMacintosh) { + return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || !!this.menubarMenus; + } + + default: + return this.windowsMainService.getWindowCount() > 0 && (!!this.menubarMenus && !!this.menubarMenus[menuId]); + } + } + + private shouldFallback(menuId: string): boolean { + return this.shouldDrawMenu(menuId) && (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow && isMacintosh); + } + + private setFallbackMenuById(menu: Electron.Menu, menuId: string): void { + switch (menuId) { + case 'File': + const newFile = new MenuItem(this.likeAction('workbench.action.files.newUntitledFile', { label: this.mnemonicLabel(nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) })); + + const newWindow = new MenuItem(this.likeAction('workbench.action.newWindow', { label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) })); + + const open = new MenuItem(this.likeAction('workbench.action.files.openFileFolder', { label: this.mnemonicLabel(nls.localize({ key: 'miOpen', comment: ['&& denotes a mnemonic'] }, "&&Open...")), click: (menuItem, win, event) => this.windowsMainService.pickFileFolderAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) })); + + const openWorkspace = new MenuItem(this.likeAction('workbench.action.openWorkspace', { label: this.mnemonicLabel(nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...")), click: (menuItem, win, event) => this.windowsMainService.pickWorkspaceAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) })); + + const openRecentMenu = new Menu(); + this.setFallbackMenuById(openRecentMenu, 'Recent'); + const openRecent = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent")), submenu: openRecentMenu }); + + menu.append(newFile); + menu.append(newWindow); + menu.append(__separator__()); + menu.append(open); + menu.append(openWorkspace); + menu.append(openRecent); + + break; + + case 'Recent': + menu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor"), 'workbench.action.reopenClosedEditor')); + + this.insertRecentMenuItems(menu); + + menu.append(__separator__()); + menu.append(this.createMenuItem(nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), 'workbench.action.openRecent')); + menu.append(__separator__()); + menu.append(new MenuItem(this.likeAction('workbench.action.clearRecentFiles', { label: this.mnemonicLabel(nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened")), click: () => this.historyMainService.clearRecentlyOpened() }))); + + break; + + case 'Help': + let twitterItem: MenuItem; + if (product.twitterUrl) { + twitterItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter")), click: () => this.openUrl(product.twitterUrl, 'openTwitterUrl') }); + } + + let featureRequestsItem: MenuItem; + if (product.requestFeatureUrl) { + featureRequestsItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests")), click: () => this.openUrl(product.requestFeatureUrl, 'openUserVoiceUrl') }); + } + + let reportIssuesItem: MenuItem; + if (product.reportIssueUrl) { + const label = nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue"); + + reportIssuesItem = new MenuItem({ label: this.mnemonicLabel(label), click: () => this.openUrl(product.reportIssueUrl, 'openReportIssues') }); + } + + let licenseItem: MenuItem; + if (product.privacyStatementUrl) { + licenseItem = new MenuItem({ + label: this.mnemonicLabel(nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License")), click: () => { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + this.openUrl(`${product.licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl'); + } else { + this.openUrl(product.licenseUrl, 'openLicenseUrl'); + } + } + }); + } + + let privacyStatementItem: MenuItem; + if (product.privacyStatementUrl) { + privacyStatementItem = new MenuItem({ + label: this.mnemonicLabel(nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement")), click: () => { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + this.openUrl(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`, 'openPrivacyStatement'); + } else { + this.openUrl(product.privacyStatementUrl, 'openPrivacyStatement'); + } + } + }); + } + + if (twitterItem) { menu.append(twitterItem); } + if (featureRequestsItem) { menu.append(featureRequestsItem); } + if (reportIssuesItem) { menu.append(reportIssuesItem); } + if ((twitterItem || featureRequestsItem || reportIssuesItem) && (licenseItem || privacyStatementItem)) { menu.append(__separator__()); } + if (licenseItem) { menu.append(licenseItem); } + if (privacyStatementItem) { menu.append(privacyStatementItem); } + + break; + } + } + + private setMenu(menu: Electron.Menu, items: Array) { + items.forEach((item: MenubarMenuItem) => { + if (isMenubarMenuItemSeparator(item)) { + menu.append(__separator__()); + } else if (isMenubarMenuItemSubmenu(item)) { + const submenu = new Menu(); + const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu: submenu }); + this.setMenu(submenu, item.submenu.items); + menu.append(submenuItem); + } else if (isMenubarMenuItemAction(item)) { + if (item.id === 'workbench.action.openRecent') { + this.insertRecentMenuItems(menu); + } else if (item.id === 'workbench.action.showAboutDialog') { + this.insertCheckForUpdatesItems(menu); + } + + // Store the keybinding + if (item.keybinding) { + this.keybindings[item.id] = item.keybinding; + } else if (this.keybindings[item.id]) { + this.keybindings[item.id] = undefined; + } + + const menuItem = this.createMenuItem(item.label, item.id, item.enabled, item.checked); + menu.append(menuItem); + } + }); + } + + private setMenuById(menu: Electron.Menu, menuId: string): void { + if (this.menubarMenus && this.menubarMenus[menuId]) { + this.setMenu(menu, this.menubarMenus[menuId].items); + } + } + + private insertCheckForUpdatesItems(menu: Electron.Menu) { + const updateItems = this.getUpdateMenuItems(); + if (updateItems.length) { + updateItems.forEach(i => menu.append(i)); + menu.append(__separator__()); + } + } + + private insertRecentMenuItems(menu: Electron.Menu) { + const { workspaces, files } = this.historyMainService.getRecentlyOpened(); + + // Workspaces + if (workspaces.length > 0) { + for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { + menu.append(this.createOpenRecentMenuItem(workspaces[i], 'openRecentWorkspace', false)); + } + + menu.append(__separator__()); + } + + // Files + if (files.length > 0) { + for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) { + menu.append(this.createOpenRecentMenuItem(files[i], 'openRecentFile', true)); + } + + menu.append(__separator__()); + } + } + + private createOpenRecentMenuItem(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, commandId: string, isFile: boolean): Electron.MenuItem { + let label: string; + let uri: URI; + if (isSingleFolderWorkspaceIdentifier(workspaceOrFile) && !isFile) { + label = unmnemonicLabel(this.labelService.getWorkspaceLabel(workspaceOrFile, { verbose: true })); + uri = workspaceOrFile; + } else if (isWorkspaceIdentifier(workspaceOrFile)) { + label = this.labelService.getWorkspaceLabel(workspaceOrFile, { verbose: true }); + uri = URI.file(workspaceOrFile.configPath); + } else { + label = unmnemonicLabel(this.labelService.getUriLabel(workspaceOrFile)); + uri = workspaceOrFile; + } + + return new MenuItem(this.likeAction(commandId, { + label, + click: (menuItem, win, event) => { + const openInNewWindow = this.isOptionClick(event); + const success = this.windowsMainService.open({ + context: OpenContext.MENU, + cli: this.environmentService.args, + urisToOpen: [uri], + forceNewWindow: openInNewWindow, + forceOpenWorkspaceAsFile: isFile + }).length > 0; + + if (!success) { + this.historyMainService.removeFromRecentlyOpened([workspaceOrFile]); + } + } + }, false)); + } + + private isOptionClick(event: Electron.Event): boolean { + return event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); + } + + private createRoleMenuItem(label: string, commandId: string, role: any): Electron.MenuItem { + const options: Electron.MenuItemConstructorOptions = { + label: this.mnemonicLabel(label), + role, + enabled: true + }; + + return new MenuItem(this.withKeybinding(commandId, options)); + } + + private setMacWindowMenu(macWindowMenu: Electron.Menu): void { + const minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsMainService.getWindowCount() > 0 }); + const zoom = new MenuItem({ label: nls.localize('mZoom', "Zoom"), role: 'zoom', enabled: this.windowsMainService.getWindowCount() > 0 }); + const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 }); + const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow'); + + const nativeTabMenuItems: Electron.MenuItem[] = []; + if (this.currentEnableNativeTabs) { + nativeTabMenuItems.push(this.createMenuItem(nls.localize('mNewTab', "New Tab"), 'workbench.action.newWindowTab')); + + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', 'selectPreviousTab')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', 'selectNextTab')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', 'moveTabToNewWindow')); + nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', 'mergeAllWindows')); + + nativeTabMenuItems.push(__separator__(), ...nativeTabMenuItems); + } + + [ + minimize, + zoom, + switchWindow, + ...nativeTabMenuItems, + __separator__(), + bringAllToFront + ].forEach(item => macWindowMenu.append(item)); + } + + private getUpdateMenuItems(): Electron.MenuItem[] { + const state = this.updateService.state; + + switch (state.type) { + case StateType.Uninitialized: + return []; + + case StateType.Idle: + return [new MenuItem({ + label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { + this.reportMenuActionTelemetry('CheckForUpdate'); + + const focusedWindow = this.windowsMainService.getFocusedWindow(); + const context = focusedWindow ? { windowId: focusedWindow.id } : null; + this.updateService.checkForUpdates(context); + }, 0) + })]; + + case StateType.CheckingForUpdates: + return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })]; + + case StateType.AvailableForDownload: + return [new MenuItem({ + label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { + this.updateService.downloadUpdate(); + } + })]; + + case StateType.Downloading: + return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; + + case StateType.Downloaded: + return [new MenuItem({ + label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { + this.reportMenuActionTelemetry('InstallUpdate'); + this.updateService.applyUpdate(); + } + })]; + + case StateType.Updating: + return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })]; + + case StateType.Ready: + return [new MenuItem({ + label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { + this.reportMenuActionTelemetry('RestartToUpdate'); + this.updateService.quitAndInstall(); + } + })]; + } + } + + private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): Electron.MenuItem; + private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): Electron.MenuItem; + private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): Electron.MenuItem { + const label = this.mnemonicLabel(arg1); + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem, win: Electron.BrowserWindow, event: Electron.Event) => { + let commandId = arg2; + if (Array.isArray(arg2)) { + commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking + } + + this.runActionInRenderer(commandId); + }; + const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0; + const checked = typeof arg4 === 'boolean' ? arg4 : false; + + const options: Electron.MenuItemConstructorOptions = { + label, + click, + enabled + }; + + if (checked) { + options['type'] = 'checkbox'; + options['checked'] = checked; + } + + let commandId: string; + if (typeof arg2 === 'string') { + commandId = arg2; + } else if (Array.isArray(arg2)) { + commandId = arg2[0]; + } + + // Add role for special case menu items + if (isMacintosh) { + if (commandId === 'editor.action.clipboardCutAction') { + options['role'] = 'cut'; + } else if (commandId === 'editor.action.clipboardCopyAction') { + options['role'] = 'copy'; + } else if (commandId === 'editor.action.clipboardPasteAction') { + options['role'] = 'paste'; + } + } + + return new MenuItem(this.withKeybinding(commandId, options)); + } + + private runActionInRenderer(id: string): void { + // We make sure to not run actions when the window has no focus, this helps + // for https://github.com/Microsoft/vscode/issues/25907 and specifically for + // https://github.com/Microsoft/vscode/issues/11928 + const activeWindow = this.windowsMainService.getFocusedWindow(); + if (activeWindow) { + this.windowsMainService.sendToFocused('vscode:runAction', { id, from: 'menu' } as IRunActionInWindowRequest); + } + } + + private withKeybinding(commandId: string, options: Electron.MenuItemConstructorOptions): Electron.MenuItemConstructorOptions { + const binding = this.keybindings[commandId]; + + // Apply binding if there is one + if (binding && binding.label) { + + // if the binding is native, we can just apply it + if (binding.isNative) { + options.accelerator = binding.label; + } + + // the keybinding is not native so we cannot show it as part of the accelerator of + // the menu item. we fallback to a different strategy so that we always display it + else { + const bindingIndex = options.label.indexOf('['); + if (bindingIndex >= 0) { + options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`; + } else { + options.label = `${options.label} [${binding.label}]`; + } + } + } + + // Unset bindings if there is none + else { + options.accelerator = void 0; + } + + return options; + } + + private likeAction(commandId: string, options: Electron.MenuItemConstructorOptions, setAccelerator = !options.accelerator): Electron.MenuItemConstructorOptions { + if (setAccelerator) { + options = this.withKeybinding(commandId, options); + } + + const originalClick = options.click; + options.click = (item, window, event) => { + this.reportMenuActionTelemetry(commandId); + if (originalClick) { + originalClick(item, window, event); + } + }; + + return options; + } + + private openUrl(url: string, id: string): void { + shell.openExternal(url); + this.reportMenuActionTelemetry(id); + } + + private reportMenuActionTelemetry(id: string): void { + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id, from: telemetryFrom }); + } + + private mnemonicLabel(label: string): string { + return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics); + } +} + +function __separator__(): Electron.MenuItem { + return new MenuItem({ type: 'separator' }); +} diff --git a/src/vs/platform/menubar/electron-main/menubarService.ts b/src/vs/platform/menubar/electron-main/menubarService.ts new file mode 100644 index 00000000000..81c1113d606 --- /dev/null +++ b/src/vs/platform/menubar/electron-main/menubarService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IMenubarService, IMenubarData, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar'; +import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; +import { ILogService } from 'vs/platform/log/common/log'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; + +export class MenubarService implements IMenubarService { + _serviceBrand: any; + + private _menubar: Menubar; + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @ILogService private logService: ILogService + ) { + // Install Menu + if (isMacintosh && isWindows) { + this._menubar = this.instantiationService.createInstance(Menubar); + } + } + + updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array): TPromise { + this.logService.trace('menubarService#updateMenubar', windowId); + + if (this._menubar) { + this._menubar.updateMenu(menus, windowId, additionalKeybindings); + } + + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/platform/menubar/node/menubarIpc.ts b/src/vs/platform/menubar/node/menubarIpc.ts new file mode 100644 index 00000000000..c75535de80c --- /dev/null +++ b/src/vs/platform/menubar/node/menubarIpc.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IMenubarService, IMenubarData, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar'; +import { Event } from 'vs/base/common/event'; + +export interface IMenubarChannel extends IChannel { + call(command: 'updateMenubar', arg: [number, IMenubarData]): TPromise; + call(command: string, arg?: any): TPromise; +} + +export class MenubarChannel implements IMenubarChannel { + + constructor(private service: IMenubarService) { } + + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'updateMenubar': return this.service.updateMenubar(arg[0], arg[1], arg[2]); + } + return undefined; + } +} + +export class MenubarChannelClient implements IMenubarService { + + _serviceBrand: any; + + constructor(private channel: IMenubarChannel) { } + + updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array): TPromise { + return this.channel.call('updateMenubar', [windowId, menus, additionalKeybindings]); + } +} \ No newline at end of file diff --git a/src/vs/platform/node/minimalTranslations.ts b/src/vs/platform/node/minimalTranslations.ts index 25a1de9e412..42e71e0a725 100644 --- a/src/vs/platform/node/minimalTranslations.ts +++ b/src/vs/platform/node/minimalTranslations.ts @@ -9,9 +9,9 @@ import { localize } from 'vs/nls'; // So that they are available for VS Code to use without downloading the entire language pack. export const minimumTranslatedStrings = { - showLanguagePackExtensions: localize('showLanguagePackExtensions', "VS Code is available in {0}. Search for language packs in the Marketplace to get started."), + showLanguagePackExtensions: localize('showLanguagePackExtensions', "Search language packs in the Marketplace to change the display language to {0}."), searchMarketplace: localize('searchMarketplace', "Search Marketplace"), - installAndRestartMessage: localize('installAndRestartMessage', "VS Code is available in {0}. Please install the language pack to change the display language."), + installAndRestartMessage: localize('installAndRestartMessage', "Install language pack to change the display language to {0}."), installAndRestart: localize('installAndRestart', "Install and Restart") }; diff --git a/src/vs/platform/node/package.ts b/src/vs/platform/node/package.ts index fff85d911f2..93c32bc7117 100644 --- a/src/vs/platform/node/package.ts +++ b/src/vs/platform/node/package.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import uri from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export interface IPackageConfiguration { name: string; version: string; } -const rootPath = path.dirname(uri.parse(require.toUrl('')).fsPath); +const rootPath = path.dirname(getPathFromAmdModule(require, '')); const packageJsonPath = path.join(rootPath, 'package.json'); -export default require.__$__nodeRequire(packageJsonPath) as IPackageConfiguration; \ No newline at end of file +export default require.__$__nodeRequire(packageJsonPath) as IPackageConfiguration; diff --git a/src/vs/platform/node/product.ts b/src/vs/platform/node/product.ts index f85d99660c5..dbb84fd473f 100644 --- a/src/vs/platform/node/product.ts +++ b/src/vs/platform/node/product.ts @@ -4,12 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import uri from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export interface IProductConfiguration { nameShort: string; nameLong: string; applicationName: string; + win32AppId: string; + win32x64AppId: string; + win32UserAppId: string; + win32x64UserAppId: string; win32AppUserModelId: string; win32MutexName: string; darwinBundleIdentifier: string; @@ -18,9 +22,11 @@ export interface IProductConfiguration { downloadUrl: string; updateUrl?: string; quality?: string; + target?: string; commit?: string; settingsSearchBuildId?: number; settingsSearchUrl?: string; + experimentsUrl?: string; date: string; extensionsGallery: { serviceUrl: string; @@ -30,7 +36,7 @@ export interface IProductConfiguration { }; extensionTips: { [id: string]: string; }; extensionImportantTips: { [id: string]: { name: string; pattern: string; }; }; - exeBasedExtensionTips: { [id: string]: any; }; + exeBasedExtensionTips: { [id: string]: { friendlyName: string, windowsPath?: string, recommendations: string[] }; }; extensionKeywords: { [extension: string]: string[]; }; extensionAllowedBadgeProviders: string[]; extensionAllowedProposedApi: string[]; @@ -73,6 +79,7 @@ export interface IProductConfiguration { 'darwin': string; }; logUploaderUrl: string; + portable?: string; } export interface ISurveyData { @@ -83,7 +90,7 @@ export interface ISurveyData { userProbability: number; } -const rootPath = path.dirname(uri.parse(require.toUrl('')).fsPath); +const rootPath = path.dirname(getPathFromAmdModule(require, '')); const productJsonPath = path.join(rootPath, 'product.json'); const product = require.__$__nodeRequire(productJsonPath) as IProductConfiguration; diff --git a/src/vs/base/test/node/zip/fixtures/extract.zip b/src/vs/platform/node/test/fixtures/extract.zip similarity index 100% rename from src/vs/base/test/node/zip/fixtures/extract.zip rename to src/vs/platform/node/test/fixtures/extract.zip diff --git a/src/vs/platform/node/test/zip.test.ts b/src/vs/platform/node/test/zip.test.ts new file mode 100644 index 00000000000..4d2e138d29a --- /dev/null +++ b/src/vs/platform/node/test/zip.test.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as os from 'os'; +import { extract } from 'vs/platform/node/zip'; +import { generateUuid } from 'vs/base/common/uuid'; +import { rimraf, exists } from 'vs/base/node/pfs'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { createCancelablePromise } from 'vs/base/common/async'; + +const fixtures = getPathFromAmdModule(require, './fixtures'); + +suite('Zip', () => { + + test('extract should handle directories', () => { + const fixture = path.join(fixtures, 'extract.zip'); + const target = path.join(os.tmpdir(), generateUuid()); + + return createCancelablePromise(token => extract(fixture, target, {}, new NullLogService(), token) + .then(() => exists(path.join(target, 'extension'))) + .then(exists => assert(exists)) + .then(() => rimraf(target))); + }); +}); diff --git a/src/vs/platform/node/zip.ts b/src/vs/platform/node/zip.ts new file mode 100644 index 00000000000..d84133cb68d --- /dev/null +++ b/src/vs/platform/node/zip.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as path from 'path'; +import { createWriteStream, WriteStream } from 'fs'; +import { Readable } from 'stream'; +import { nfcall, ninvoke, SimpleThrottler, createCancelablePromise } from 'vs/base/common/async'; +import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { open as _openZip, Entry, ZipFile } from 'yauzl'; +import * as yazl from 'yazl'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { once } from 'vs/base/common/event'; + +export interface IExtractOptions { + overwrite?: boolean; + + /** + * Source path within the ZIP archive. Only the files contained in this + * path will be extracted. + */ + sourcePath?: string; +} + +interface IOptions { + sourcePathRegex: RegExp; +} + +export type ExtractErrorType = 'CorruptZip' | 'Incomplete'; + +export class ExtractError extends Error { + + readonly type: ExtractErrorType; + readonly cause: Error; + + constructor(type: ExtractErrorType, cause: Error) { + let message = cause.message; + + switch (type) { + case 'CorruptZip': message = `Corrupt ZIP: ${message}`; break; + } + + super(message); + this.type = type; + this.cause = cause; + } +} + +function modeFromEntry(entry: Entry) { + let attr = entry.externalFileAttributes >> 16 || 33188; + + return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */] + .map(mask => attr & mask) + .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */); +} + +function toExtractError(err: Error): ExtractError { + if (err instanceof ExtractError) { + return err; + } + + let type: ExtractErrorType = void 0; + + if (/end of central directory record signature not found/.test(err.message)) { + type = 'CorruptZip'; + } + + return new ExtractError(type, err); +} + +function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions, token: CancellationToken): TPromise { + const dirName = path.dirname(fileName); + const targetDirName = path.join(targetPath, dirName); + if (targetDirName.indexOf(targetPath) !== 0) { + return TPromise.wrapError(new Error(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName))); + } + const targetFileName = path.join(targetPath, fileName); + + let istream: WriteStream; + + once(token.onCancellationRequested)(() => { + if (istream) { + istream.close(); + } + }); + + return mkdirp(targetDirName, void 0, token).then(() => new TPromise((c, e) => { + if (token.isCancellationRequested) { + return; + } + + istream = createWriteStream(targetFileName, { mode }); + istream.once('close', () => c(null)); + istream.once('error', e); + stream.once('error', e); + stream.pipe(istream); + })); +} + +function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, logService: ILogService, token: CancellationToken): Promise { + let last = createCancelablePromise(() => Promise.resolve(null)); + let extractedEntriesCount = 0; + + once(token.onCancellationRequested)(() => { + logService.debug(targetPath, 'Cancelled.'); + last.cancel(); + zipfile.close(); + }); + + return new Promise((c, e) => { + const throttler = new SimpleThrottler(); + + const readNextEntry = (token: CancellationToken) => { + if (token.isCancellationRequested) { + return; + } + + extractedEntriesCount++; + zipfile.readEntry(); + }; + + zipfile.once('error', e); + zipfile.once('close', () => last.then(() => { + if (token.isCancellationRequested || zipfile.entryCount === extractedEntriesCount) { + c(null); + } else { + e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount)))); + } + }, e)); + zipfile.readEntry(); + zipfile.on('entry', (entry: Entry) => { + + if (token.isCancellationRequested) { + return; + } + + if (!options.sourcePathRegex.test(entry.fileName)) { + readNextEntry(token); + return; + } + + const fileName = entry.fileName.replace(options.sourcePathRegex, ''); + + // directory file names end with '/' + if (/\/$/.test(fileName)) { + const targetFileName = path.join(targetPath, fileName); + last = createCancelablePromise(token => mkdirp(targetFileName, void 0, token).then(() => readNextEntry(token))); + return; + } + + const stream = ninvoke(zipfile, zipfile.openReadStream, entry); + const mode = modeFromEntry(entry); + + last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token))))); + }); + }); +} + +function openZip(zipFile: string, lazy: boolean = false): TPromise { + return nfcall(_openZip, zipFile, lazy ? { lazyEntries: true } : void 0) + .then(null, err => TPromise.wrapError(toExtractError(err))); +} + +export interface IFile { + path: string; + contents?: Buffer | string; + localPath?: string; +} + +export function zip(zipPath: string, files: IFile[]): TPromise { + return new TPromise((c, e) => { + const zip = new yazl.ZipFile(); + files.forEach(f => f.contents ? zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path) : zip.addFile(f.localPath, f.path)); + zip.end(); + + const zipStream = createWriteStream(zipPath); + zip.outputStream.pipe(zipStream); + + zip.outputStream.once('error', e); + zipStream.once('error', e); + zipStream.once('finish', () => c(zipPath)); + }); +} + +export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, logService: ILogService, token: CancellationToken): TPromise { + const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); + + let promise = openZip(zipPath, true); + + if (options.overwrite) { + promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile)); + } + + return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, logService, token)); +} + +function read(zipPath: string, filePath: string): TPromise { + return openZip(zipPath).then(zipfile => { + return new TPromise((c, e) => { + zipfile.on('entry', (entry: Entry) => { + if (entry.fileName === filePath) { + ninvoke(zipfile, zipfile.openReadStream, entry).then(stream => c(stream), err => e(err)); + } + }); + + zipfile.once('close', () => e(new Error(nls.localize('notFound', "{0} not found inside zip.", filePath)))); + }); + }); +} + +export function buffer(zipPath: string, filePath: string): TPromise { + return read(zipPath, filePath).then(stream => { + return new TPromise((c, e) => { + const buffers: Buffer[] = []; + stream.once('error', e); + stream.on('data', b => buffers.push(b as Buffer)); + stream.on('end', () => c(Buffer.concat(buffers))); + }); + }); +} diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 4b1315c077f..525d8f18a25 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -205,7 +205,7 @@ export class NoOpNotification implements INotificationHandle { private readonly _onDidClose: Emitter = new Emitter(); - public get onDidClose(): Event { + get onDidClose(): Event { return this._onDidClose.event; } diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 44e16595037..898fea0337e 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/output/node/outputAppender.ts b/src/vs/platform/output/node/outputAppender.ts new file mode 100644 index 00000000000..a7ffc9cd3d2 --- /dev/null +++ b/src/vs/platform/output/node/outputAppender.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { createRotatingLogger } from 'vs/platform/log/node/spdlogService'; +import { RotatingLogger } from 'spdlog'; + +export class OutputAppender { + + private appender: RotatingLogger; + + constructor(name: string, file: string) { + this.appender = createRotatingLogger(name, file, 1024 * 1024 * 30, 1); + this.appender.clearFormatters(); + } + + append(content: string): void { + this.appender.critical(content); + } + + flush(): void { + this.appender.flush(); + } +} \ No newline at end of file diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index a227d502d4b..405170fa1e1 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; @@ -24,7 +23,7 @@ export interface IProgressService { * Indicate progress for the duration of the provided promise. Progress will stop in * any case of promise completion, error or cancellation. */ - showWhile(promise: TPromise, delay?: number): TPromise; + showWhile(promise: Thenable, delay?: number): Thenable; } export interface IProgressRunner { @@ -64,36 +63,6 @@ export class Progress implements IProgress { } } -export enum ProgressLocation { - Explorer = 1, - Scm = 3, - Extensions = 5, - Window = 10, - Notification = 15 -} - -export interface IProgressOptions { - location: ProgressLocation; - title?: string; - source?: string; - total?: number; - cancellable?: boolean; -} - -export interface IProgressStep { - message?: string; - increment?: number; -} - -export const IProgressService2 = createDecorator('progressService2'); - -export interface IProgressService2 { - - _serviceBrand: any; - - withProgress

, R=any>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P; -} - /** * A helper to show progress during a long running operation. If the operation * is started multiple times, only the last invocation will drive the progress. @@ -156,4 +125,4 @@ export class LongRunningOperation { dispose(): void { this.currentOperationDisposables = dispose(this.currentOperationDisposables); } -} \ No newline at end of file +} diff --git a/src/vs/platform/quickOpen/common/quickOpen.ts b/src/vs/platform/quickOpen/common/quickOpen.ts index e5e1bffd9d4..b0b91b9e1b4 100644 --- a/src/vs/platform/quickOpen/common/quickOpen.ts +++ b/src/vs/platform/quickOpen/common/quickOpen.ts @@ -5,86 +5,9 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import uri from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; +import { IQuickNavigateConfiguration, IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IAction } from 'vs/base/common/actions'; -import { FileKind } from 'vs/platform/files/common/files'; - -export interface IFilePickOpenEntry extends IPickOpenEntry { - resource: uri; - fileKind?: FileKind; -} - -export interface IPickOpenAction extends IAction { - run(item: IPickOpenItem): TPromise; -} - -export interface IPickOpenEntry { - id?: string; - label: string; - description?: string; - detail?: string; - tooltip?: string; - separator?: ISeparator; - alwaysShow?: boolean; - run?: (context: IEntryRunContext) => void; - action?: IAction; - payload?: any; -} - -export interface IPickOpenItem { - index: number; - remove: () => void; - getId: () => string; - getResource: () => uri; - getPayload: () => any; -} - -export interface ISeparator { - border?: boolean; - label?: string; -} - -export interface IPickOptions { - - /** - * an optional string to show as place holder in the input box to guide the user what she picks on - */ - placeHolder?: string; - - /** - * optional auto focus settings - */ - autoFocus?: IAutoFocus; - - /** - * an optional flag to include the description when filtering the picks - */ - matchOnDescription?: boolean; - - /** - * an optional flag to include the detail when filtering the picks - */ - matchOnDetail?: boolean; - - /** - * an optional flag to not close the picker on focus lost - */ - ignoreFocusLost?: boolean; - - /** - * enables quick navigate in the picker to open an element without typing - */ - quickNavigateConfiguration?: IQuickNavigateConfiguration; - - /** - * a context key to set when this picker is active - */ - contextKey?: string; -} export interface IShowOptions { quickNavigateConfiguration?: IQuickNavigateConfiguration; @@ -107,18 +30,6 @@ export interface IQuickOpenService { */ show(prefix?: string, options?: IShowOptions): TPromise; - /** - * A convenient way to bring up quick open as a picker with custom elements. This bypasses the quick open handler - * registry and just leverages the quick open widget to select any kind of entries. - * - * Passing in a promise will allow you to resolve the elements in the background while quick open will show a - * progress bar spinning. - */ - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; - pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; - /** * Allows to navigate from the outside in an opened picker. */ diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 14edbfdfa22..e6d636c72e4 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -8,20 +8,36 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { TPromise } from 'vs/base/common/winjs.base'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; -export interface IPickOpenEntry { +export interface IQuickPickItem { + type?: 'item'; id?: string; label: string; description?: string; detail?: string; + iconClasses?: string[]; + buttons?: IQuickInputButton[]; picked?: boolean; + alwaysShow?: boolean; +} + +export interface IQuickPickSeparator { + type: 'separator'; + label?: string; +} + +export interface IKeyMods { + readonly ctrlCmd: boolean; + readonly alt: boolean; } export interface IQuickNavigateConfiguration { keybindings: ResolvedKeybinding[]; } -export interface IPickOptions { +export interface IPickOptions { /** * an optional string to show as place holder in the input box to guide the user what she picks on @@ -47,6 +63,25 @@ export interface IPickOptions { * an optional flag to make this picker multi-select */ canPickMany?: boolean; + + /** + * enables quick navigate in the picker to open an element without typing + */ + quickNavigate?: IQuickNavigateConfiguration; + + /** + * a context key to set when this picker is active + */ + contextKey?: string; + + /** + * an optional property for the item to focus initially. + */ + activeItem?: TPromise | T; + + onKeyMods?: (keyMods: IKeyMods) => void; + onDidFocus?: (entry: T) => void; + onDidTriggerItemButton?: (context: IQuickPickItemButtonContext) => void; } export interface IInputOptions { @@ -81,29 +116,135 @@ export interface IInputOptions { /** * an optional function that is used to validate user input. */ - validateInput?: (input: string) => TPromise; + validateInput?: (input: string) => Thenable; } export interface IQuickInput { + title: string | undefined; + + step: number | undefined; + + totalSteps: number | undefined; + + enabled: boolean; + + contextKey: string | undefined; + + busy: boolean; + + ignoreFocusOut: boolean; + + show(): void; + + hide(): void; + + onDidHide: Event; + + dispose(): void; +} + +export interface IQuickPick extends IQuickInput { + + value: string; + + placeholder: string | undefined; + + readonly onDidChangeValue: Event; + + readonly onDidAccept: Event; + + buttons: ReadonlyArray; + + readonly onDidTriggerButton: Event; + + readonly onDidTriggerItemButton: Event>; + + items: ReadonlyArray; + + canSelectMany: boolean; + + matchOnDescription: boolean; + + matchOnDetail: boolean; + + quickNavigate: IQuickNavigateConfiguration | undefined; + + activeItems: ReadonlyArray; + + readonly onDidChangeActive: Event; + + selectedItems: ReadonlyArray; + + readonly onDidChangeSelection: Event; + + readonly keyMods: IKeyMods; +} + +export interface IInputBox extends IQuickInput { + + value: string; + + valueSelection: Readonly<[number, number]>; + + placeholder: string | undefined; + + password: boolean; + + readonly onDidChangeValue: Event; + + readonly onDidAccept: Event; + + buttons: ReadonlyArray; + + readonly onDidTriggerButton: Event; + + prompt: string | undefined; + + validationMessage: string | undefined; +} + +export interface IQuickInputButton { + iconPath?: { dark: URI; light?: URI; }; + iconClass?: string; + tooltip?: string; +} + +export interface IQuickPickItemButtonEvent { + button: IQuickInputButton; + item: T; +} + +export interface IQuickPickItemButtonContext extends IQuickPickItemButtonEvent { + removeItem(): void; +} + +export const IQuickInputService = createDecorator('quickInputService'); + +export type Omit = Pick>; + +export type QuickPickInput = T | IQuickPickSeparator; + +export interface IQuickInputService { + + _serviceBrand: any; + /** * Opens the quick input box for selecting items and returns a promise with the user selected item(s) if any. */ - pick(picks: TPromise, options?: O, token?: CancellationToken): TPromise; + pick(picks: TPromise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): TPromise; + pick(picks: TPromise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): TPromise; + pick(picks: TPromise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): TPromise; /** * Opens the quick input box for text input and returns a promise with the user typed value if any. */ input(options?: IInputOptions, token?: CancellationToken): TPromise; -} -export const IQuickInputService = createDecorator('quickInputService'); + backButton: IQuickInputButton; -export interface IQuickInputService extends IQuickInput { - - _serviceBrand: any; - - multiStepInput(handler: (input: IQuickInput, token: CancellationToken) => Thenable, token?: CancellationToken): Thenable; + createQuickPick(): IQuickPick; + createInputBox(): IInputBox; focus(): void; @@ -113,5 +254,7 @@ export interface IQuickInputService extends IQuickInput { accept(): TPromise; + back(): TPromise; + cancel(): TPromise; } diff --git a/src/vs/platform/request/electron-browser/requestService.ts b/src/vs/platform/request/electron-browser/requestService.ts index 265f57f3d46..c66762f4d49 100644 --- a/src/vs/platform/request/electron-browser/requestService.ts +++ b/src/vs/platform/request/electron-browser/requestService.ts @@ -8,18 +8,20 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IRequestOptions, IRequestContext, IRequestFunction } from 'vs/base/node/request'; import { Readable } from 'stream'; import { RequestService as NodeRequestService } from 'vs/platform/request/node/requestService'; +import { CancellationToken } from 'vscode'; +import { canceled } from 'vs/base/common/errors'; /** * This service exposes the `request` API, while using the global * or configured proxy settings. */ export class RequestService extends NodeRequestService { - request(options: IRequestOptions): TPromise { - return super.request(options, xhrRequest); + request(options: IRequestOptions, token: CancellationToken): TPromise { + return super.request(options, token, xhrRequest); } } -export const xhrRequest: IRequestFunction = (options: IRequestOptions): TPromise => { +export const xhrRequest: IRequestFunction = (options: IRequestOptions, token: CancellationToken): TPromise => { const xhr = new XMLHttpRequest(); return new TPromise((resolve, reject) => { @@ -66,12 +68,14 @@ export const xhrRequest: IRequestFunction = (options: IRequestOptions): TPromise xhr.timeout = options.timeout; } - xhr.send(options.data); - return null; + // TODO: remove any + xhr.send(options.data as any); - }, () => { // cancel - xhr.abort(); + token.onCancellationRequested(() => { + xhr.abort(); + reject(canceled()); + }); }); }; diff --git a/src/vs/platform/request/electron-main/requestService.ts b/src/vs/platform/request/electron-main/requestService.ts index 2d583abe0cd..e1b05b56ac6 100644 --- a/src/vs/platform/request/electron-main/requestService.ts +++ b/src/vs/platform/request/electron-main/requestService.ts @@ -9,6 +9,7 @@ import { IRequestOptions, IRequestContext, request, IRawRequestFunction } from ' import { RequestService as NodeRequestService } from 'vs/platform/request/node/requestService'; import { assign } from 'vs/base/common/objects'; import { net } from 'electron'; +import { CancellationToken } from 'vs/base/common/cancellation'; function getRawRequest(options: IRequestOptions): IRawRequestFunction { return net.request as any as IRawRequestFunction; @@ -16,7 +17,7 @@ function getRawRequest(options: IRequestOptions): IRawRequestFunction { export class RequestService extends NodeRequestService { - request(options: IRequestOptions): TPromise { - return super.request(options, options => request(assign({}, options || {}, { getRawRequest }))); + request(options: IRequestOptions, token: CancellationToken): TPromise { + return super.request(options, token, options => request(assign({}, options || {}, { getRawRequest }), token)); } -} \ No newline at end of file +} diff --git a/src/vs/platform/request/node/request.ts b/src/vs/platform/request/node/request.ts index b8d501fa2b5..a7bbfc284e5 100644 --- a/src/vs/platform/request/node/request.ts +++ b/src/vs/platform/request/node/request.ts @@ -10,13 +10,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IRequestOptions, IRequestContext } from 'vs/base/node/request'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const IRequestService = createDecorator('requestService2'); export interface IRequestService { _serviceBrand: any; - request(options: IRequestOptions): TPromise; + request(options: IRequestOptions, token: CancellationToken): TPromise; } export interface IHTTPConfiguration { @@ -37,12 +38,12 @@ Registry.as(Extensions.Configuration) 'http.proxy': { type: 'string', pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+)(:\\d+)?/?$|^$', - description: localize('proxy', "The proxy setting to use. If not set will be taken from the http_proxy and https_proxy environment variables") + description: localize('proxy', "The proxy setting to use. If not set will be taken from the http_proxy and https_proxy environment variables.") }, 'http.proxyStrictSSL': { type: 'boolean', default: true, - description: localize('strictSSL', "Whether the proxy server certificate should be verified against the list of supplied CAs.") + description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs.") }, 'http.proxyAuthorization': { type: ['null', 'string'], @@ -50,4 +51,4 @@ Registry.as(Extensions.Configuration) description: localize('proxyAuthorization', "The value to send as the 'Proxy-Authorization' header for every network request.") } } - }); \ No newline at end of file + }); diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 0f57b4e35ba..711f4561315 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -12,6 +12,7 @@ import { getProxyAgent } from 'vs/base/node/proxy'; import { IRequestService, IHTTPConfiguration } from 'vs/platform/request/node/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; /** * This service exposes the `request` API, while using the global @@ -40,18 +41,21 @@ export class RequestService implements IRequestService { this.authorization = config.http && config.http.proxyAuthorization; } - async request(options: IRequestOptions, requestFn: IRequestFunction = request): TPromise { + request(options: IRequestOptions, token: CancellationToken, requestFn: IRequestFunction = request): TPromise { this.logService.trace('RequestService#request', options.url); const { proxyUrl, strictSSL } = this; + const agentPromise = options.agent ? TPromise.wrap(options.agent) : TPromise.wrap(getProxyAgent(options.url, { proxyUrl, strictSSL })); - options.agent = options.agent || await getProxyAgent(options.url, { proxyUrl, strictSSL }); - options.strictSSL = strictSSL; + return agentPromise.then(agent => { + options.agent = agent; + options.strictSSL = strictSSL; - if (this.authorization) { - options.headers = assign(options.headers || {}, { 'Proxy-Authorization': this.authorization }); - } + if (this.authorization) { + options.headers = assign(options.headers || {}, { 'Proxy-Authorization': this.authorization }); + } - return requestFn(options); + return requestFn(options, token); + }); } } diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index ded634a58a4..6196383c8df 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -9,10 +9,11 @@ import * as glob from 'vs/base/common/glob'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as paths from 'vs/base/common/paths'; -import uri, { UriComponents } from 'vs/base/common/uri'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { URI as uri, UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const VIEW_ID = 'workbench.view.search'; @@ -24,10 +25,10 @@ export const ISearchService = createDecorator('searchService'); */ export interface ISearchService { _serviceBrand: any; - search(query: ISearchQuery): PPromise; + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise; extendQuery(query: ISearchQuery): void; clearCache(cacheKey: string): TPromise; - registerSearchResultProvider(provider: ISearchResultProvider): IDisposable; + registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable; } export interface ISearchHistoryValues { @@ -45,8 +46,18 @@ export interface ISearchHistoryService { save(history: ISearchHistoryValues): void; } +/** + * TODO@roblou - split text from file search entirely, or share code in a more natural way. + */ +export const enum SearchProviderType { + file, + fileIndex, + text +} + export interface ISearchResultProvider { - search(query: ISearchQuery): PPromise; + search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise; + clearCache(cacheKey: string): TPromise; } export interface IFolderQuery { @@ -75,6 +86,7 @@ export interface ICommonQueryOptions { disregardExcludeSettings?: boolean; ignoreSymlinks?: boolean; maxFileSize?: number; + previewOptions?: ITextSearchPreviewOptions; } export interface IQueryOptions extends ICommonQueryOptions { @@ -95,7 +107,7 @@ export interface ISearchQueryProps extends ICommonQuery export type ISearchQuery = ISearchQueryProps; export type IRawSearchQuery = ISearchQueryProps; -export enum QueryType { +export const enum QueryType { File = 1, Text = 2 } @@ -122,15 +134,33 @@ export interface IPatternInfo { export interface IFileMatch { resource?: U; - lineMatches?: ILineMatch[]; + matches?: ITextSearchResult[]; } export type IRawFileMatch2 = IFileMatch; -export interface ILineMatch { - preview: string; - lineNumber: number; - offsetAndLengths: number[][]; +export interface ITextSearchPreviewOptions { + maxLines: number; + leadingChars: number; + totalChars: number; +} + +export interface ISearchRange { + readonly startLineNumber: number; + readonly startColumn: number; + readonly endLineNumber: number; + readonly endColumn: number; +} + +export interface ITextSearchResultPreview { + text: string; + match: ISearchRange; +} + +export interface ITextSearchResult { + uri?: uri; + range: ISearchRange; + preview: ITextSearchResultPreview; } export interface IProgress { @@ -145,53 +175,97 @@ export interface ISearchProgressItem extends IFileMatch, IProgress { export interface ISearchCompleteStats { limitHit?: boolean; - stats?: ISearchStats; + stats?: IFileSearchStats | ITextSearchStats; } export interface ISearchComplete extends ISearchCompleteStats { results: IFileMatch[]; } -export interface ISearchStats { +export interface ITextSearchStats { + type: 'textSearchProvider' | 'searchProcess'; +} + +export interface IFileSearchStats { fromCache: boolean; + detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats | IFileIndexProviderStats; + resultCount: number; - unsortedResultTime?: number; - sortedResultTime?: number; + type: 'fileIndexProvider' | 'fileSearchProvider' | 'searchProcess'; + sortingTime?: number; } -export interface ICachedSearchStats extends ISearchStats { - cacheLookupStartTime: number; - cacheFilterStartTime: number; - cacheLookupResultTime: number; +export interface ICachedSearchStats { + cacheWasResolved: boolean; + cacheLookupTime: number; + cacheFilterTime: number; cacheEntryCount: number; - joined?: ISearchStats; } -export interface IUncachedSearchStats extends ISearchStats { +export interface ISearchEngineStats { traversal: string; - errors: string[]; - fileWalkStartTime: number; - fileWalkResultTime: number; + fileWalkTime: number; directoriesWalked: number; filesWalked: number; - cmdForkStartTime?: number; - cmdForkResultTime?: number; + cmdTime: number; cmdResultCount?: number; } +export interface IFileSearchProviderStats { + providerTime: number; + postProcessTime: number; +} -// ---- very simple implementation of the search model -------------------- +export interface IFileIndexProviderStats { + providerTime: number; + providerResultCount: number; + fileWalkTime: number; + directoriesWalked: number; + filesWalked: number; +} export class FileMatch implements IFileMatch { - public lineMatches: LineMatch[] = []; + public matches: ITextSearchResult[] = []; constructor(public resource: uri) { // empty } } -export class LineMatch implements ILineMatch { - constructor(public preview: string, public lineNumber: number, public offsetAndLengths: number[][]) { - // empty +export class TextSearchResult implements ITextSearchResult { + range: ISearchRange; + preview: ITextSearchResultPreview; + + constructor(fullLine: string, range: ISearchRange, previewOptions?: ITextSearchPreviewOptions) { + this.range = range; + if (previewOptions) { + const previewStart = Math.max(range.startColumn - previewOptions.leadingChars, 0); + const previewEnd = previewOptions.totalChars + previewStart; + const endOfMatchRangeInPreview = Math.min(previewEnd, range.endColumn - previewStart); + + this.preview = { + text: fullLine.substring(previewStart, previewEnd), + match: new OneLineRange(0, range.startColumn - previewStart, endOfMatchRangeInPreview) + }; + } else { + this.preview = { + text: fullLine, + match: new OneLineRange(0, range.startColumn, range.endColumn) + }; + } + } +} + +export class OneLineRange implements ISearchRange { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + + constructor(lineNumber: number, startColumn: number, endColumn: number) { + this.startLineNumber = lineNumber; + this.startColumn = startColumn; + this.endLineNumber = lineNumber; + this.endColumn = endColumn; } } diff --git a/src/vs/platform/search/test/common/search.test.ts b/src/vs/platform/search/test/common/search.test.ts new file mode 100644 index 00000000000..c56b87b33a8 --- /dev/null +++ b/src/vs/platform/search/test/common/search.test.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { TextSearchResult, OneLineRange, ITextSearchResult, ITextSearchPreviewOptions } from 'vs/platform/search/common/search'; + +suite('TextSearchResult', () => { + + const previewOptions1: ITextSearchPreviewOptions = { + leadingChars: 10, + maxLines: 1, + totalChars: 100 + }; + + test('empty', () => { + assert.deepEqual( + new TextSearchResult('', new OneLineRange(5, 0, 0)), + { + preview: { + text: '', + match: new OneLineRange(0, 0, 0) + }, + range: new OneLineRange(5, 0, 0) + }); + + assert.deepEqual( + new TextSearchResult('', new OneLineRange(5, 0, 0), previewOptions1), + { + preview: { + text: '', + match: new OneLineRange(0, 0, 0) + }, + range: new OneLineRange(5, 0, 0) + }); + }); + + test('short', () => { + assert.deepEqual( + new TextSearchResult('foo bar', new OneLineRange(5, 4, 7)), + { + preview: { + text: 'foo bar', + match: new OneLineRange(0, 4, 7) + }, + range: new OneLineRange(5, 4, 7) + }); + + assert.deepEqual( + new TextSearchResult('foo bar', new OneLineRange(5, 4, 7), previewOptions1), + { + preview: { + text: 'foo bar', + match: new OneLineRange(0, 4, 7) + }, + range: new OneLineRange(5, 4, 7) + }); + }); + + test('leading', () => { + assert.deepEqual( + new TextSearchResult('long text very long text foo', new OneLineRange(5, 25, 28), previewOptions1), + { + preview: { + text: 'long text foo', + match: new OneLineRange(0, 10, 13) + }, + range: new OneLineRange(5, 25, 28) + }); + }); + + test('trailing', () => { + assert.deepEqual( + new TextSearchResult('foo long text very long text long text very long text long text very long text long text very long text long text very long text', new OneLineRange(5, 0, 3), previewOptions1), + { + preview: { + text: 'foo long text very long text long text very long text long text very long text long text very long t', + match: new OneLineRange(0, 0, 3) + }, + range: new OneLineRange(5, 0, 3) + }); + }); + + test('middle', () => { + assert.deepEqual( + new TextSearchResult('long text very long text long foo text very long text long text very long text long text very long text long text very long text', new OneLineRange(5, 30, 33), previewOptions1), + { + preview: { + text: 'text long foo text very long text long text very long text long text very long text long text very l', + match: new OneLineRange(0, 10, 13) + }, + range: new OneLineRange(5, 30, 33) + }); + }); + + test('truncating match', () => { + const previewOptions: ITextSearchPreviewOptions = { + leadingChars: 4, + maxLines: 1, + totalChars: 5 + }; + + assert.deepEqual( + new TextSearchResult('foo bar', new OneLineRange(0, 4, 7), previewOptions), + { + preview: { + text: 'foo b', + match: new OneLineRange(0, 4, 5) + }, + range: new OneLineRange(0, 4, 7) + }); + }); +}); \ No newline at end of file diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 12e1682db58..5238541eac1 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -6,7 +6,7 @@ 'use strict'; import * as path from 'path'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { writeFileAndFlushSync } from 'vs/base/node/extfs'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; @@ -25,7 +25,7 @@ export class FileStorage { } } - public getItem(key: string, defaultValue?: T): T { + getItem(key: string, defaultValue?: T): T { this.ensureLoaded(); const res = this.database[key]; @@ -36,7 +36,7 @@ export class FileStorage { return res; } - public setItem(key: string, data: any): void { + setItem(key: string, data: any): void { this.ensureLoaded(); // Remove an item when it is undefined or null @@ -55,7 +55,7 @@ export class FileStorage { this.saveSync(); } - public removeItem(key: string): void { + removeItem(key: string): void { this.ensureLoaded(); // Only update if the key is actually present (not undefined) @@ -96,15 +96,15 @@ export class StateService implements IStateService { this.fileStorage = new FileStorage(path.join(environmentService.userDataPath, 'storage.json'), error => logService.error(error)); } - public getItem(key: string, defaultValue?: T): T { + getItem(key: string, defaultValue?: T): T { return this.fileStorage.getItem(key, defaultValue); } - public setItem(key: string, data: any): void { + setItem(key: string, data: any): void { this.fileStorage.setItem(key, data); } - public removeItem(key: string): void { + removeItem(key: string): void { this.fileStorage.removeItem(key); } } diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index fffa1184e33..ec01e18ab13 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -11,7 +11,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService'; export const IStatusbarService = createDecorator('statusbarService'); -export enum StatusbarAlignment { +export const enum StatusbarAlignment { LEFT, RIGHT } diff --git a/src/vs/platform/storage/common/migration.ts b/src/vs/platform/storage/common/migration.ts index 652ad4fdf5a..0e7a42f6e6c 100644 --- a/src/vs/platform/storage/common/migration.ts +++ b/src/vs/platform/storage/common/migration.ts @@ -7,7 +7,7 @@ import { IStorage, StorageService } from 'vs/platform/storage/common/storageService'; import { endsWith, startsWith, rtrim } from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; /** diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 3f42a96053d..5d0c1161567 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -54,7 +54,7 @@ export interface IStorageService { getBoolean(key: string, scope?: StorageScope, defaultValue?: boolean): boolean; } -export enum StorageScope { +export const enum StorageScope { /** * The stored data will be scoped to all workspaces of this domain. diff --git a/src/vs/platform/storage/common/storageService.ts b/src/vs/platform/storage/common/storageService.ts index a15769237a5..cd60f29cd2e 100644 --- a/src/vs/platform/storage/common/storageService.ts +++ b/src/vs/platform/storage/common/storageService.ts @@ -21,13 +21,13 @@ export interface IStorage { export class StorageService implements IStorageService { - public _serviceBrand: any; + _serviceBrand: any; - public static readonly COMMON_PREFIX = 'storage://'; - public static readonly GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`; - public static readonly WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`; - public static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier'; - public static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__'; + static readonly COMMON_PREFIX = 'storage://'; + static readonly GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`; + static readonly WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`; + static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier'; + static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__'; private _workspaceStorage: IStorage; private _globalStorage: IStorage; @@ -47,11 +47,11 @@ export class StorageService implements IStorageService { this.setWorkspaceId(workspaceId, legacyWorkspaceId); } - public get workspaceId(): string { + get workspaceId(): string { return this._workspaceId; } - public setWorkspaceId(workspaceId: string, legacyWorkspaceId?: number): void { + setWorkspaceId(workspaceId: string, legacyWorkspaceId?: number): void { this._workspaceId = workspaceId; // Calculate workspace storage key @@ -64,11 +64,11 @@ export class StorageService implements IStorageService { } } - public get globalStorage(): IStorage { + get globalStorage(): IStorage { return this._globalStorage; } - public get workspaceStorage(): IStorage { + get workspaceStorage(): IStorage { return this._workspaceStorage; } @@ -124,7 +124,7 @@ export class StorageService implements IStorageService { } } - public store(key: string, value: any, scope = StorageScope.GLOBAL): void { + store(key: string, value: any, scope = StorageScope.GLOBAL): void { const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; if (types.isUndefinedOrNull(value)) { @@ -142,7 +142,7 @@ export class StorageService implements IStorageService { } } - public get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string { + get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string { const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; const value = storage.getItem(this.toStorageKey(key, scope)); @@ -153,7 +153,7 @@ export class StorageService implements IStorageService { return value; } - public getInteger(key: string, scope = StorageScope.GLOBAL, defaultValue?: number): number { + getInteger(key: string, scope = StorageScope.GLOBAL, defaultValue?: number): number { const value = this.get(key, scope, defaultValue); if (types.isUndefinedOrNull(value)) { @@ -163,7 +163,7 @@ export class StorageService implements IStorageService { return parseInt(value, 10); } - public getBoolean(key: string, scope = StorageScope.GLOBAL, defaultValue?: boolean): boolean { + getBoolean(key: string, scope = StorageScope.GLOBAL, defaultValue?: boolean): boolean { const value = this.get(key, scope, defaultValue); if (types.isUndefinedOrNull(value)) { @@ -177,7 +177,7 @@ export class StorageService implements IStorageService { return value ? true : false; } - public remove(key: string, scope = StorageScope.GLOBAL): void { + remove(key: string, scope = StorageScope.GLOBAL): void { const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; const storageKey = this.toStorageKey(key, scope); @@ -201,11 +201,11 @@ export class InMemoryLocalStorage implements IStorage { this.store = {}; } - public get length() { + get length() { return Object.keys(this.store).length; } - public key(index: number): string { + key(index: number): string { const keys = Object.keys(this.store); if (keys.length > index) { return keys[index]; @@ -214,11 +214,11 @@ export class InMemoryLocalStorage implements IStorage { return null; } - public setItem(key: string, value: any): void { + setItem(key: string, value: any): void { this.store[key] = value.toString(); } - public getItem(key: string): string { + getItem(key: string): string { const item = this.store[key]; if (!types.isUndefinedOrNull(item)) { return item; @@ -227,7 +227,7 @@ export class InMemoryLocalStorage implements IStorage { return null; } - public removeItem(key: string): void { + removeItem(key: string): void { delete this.store[key]; } } diff --git a/src/vs/platform/storage/test/browser/migration.test.ts b/src/vs/platform/storage/test/browser/migration.test.ts index 2750a3b2699..9e866cc4ca7 100644 --- a/src/vs/platform/storage/test/browser/migration.test.ts +++ b/src/vs/platform/storage/test/browser/migration.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { StorageService } from 'vs/platform/storage/common/storageService'; import { parseStorage, migrateStorageToMultiRootWorkspace } from 'vs/platform/storage/common/migration'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { StorageScope } from 'vs/platform/storage/common/storage'; import { startsWith } from 'vs/base/common/strings'; diff --git a/src/vs/platform/telemetry/common/experiments.ts b/src/vs/platform/telemetry/common/experiments.ts deleted file mode 100644 index 861a0a14621..00000000000 --- a/src/vs/platform/telemetry/common/experiments.ts +++ /dev/null @@ -1,92 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { deepClone } from 'vs/base/common/objects'; - -/* __GDPR__FRAGMENT__ - "IExperiments" : { - } -*/ -export interface IExperiments { -} - -export const IExperimentService = createDecorator('experimentService'); - -export interface IExperimentService { - - _serviceBrand: any; - - getExperiments(): IExperiments; -} - -export class ExperimentService implements IExperimentService { - - _serviceBrand: any; - - private experiments: IExperiments = {}; // Shortcut while there are no experiments. - - constructor( - @IStorageService private storageService: IStorageService, - @IConfigurationService private configurationService: IConfigurationService, - ) { } - - getExperiments() { - if (!this.experiments) { - this.experiments = loadExperiments(this.storageService, this.configurationService); - } - return this.experiments; - } -} - -function loadExperiments(storageService: IStorageService, configurationService: IConfigurationService): IExperiments { - const experiments = splitExperimentsRandomness(storageService); - return applyOverrides(experiments, configurationService); -} - -function applyOverrides(experiments: IExperiments, configurationService: IConfigurationService): IExperiments { - const experimentsConfig = getExperimentsOverrides(configurationService); - Object.keys(experiments).forEach(key => { - if (key in experimentsConfig) { - experiments[key] = experimentsConfig[key]; - } - }); - return experiments; -} - -function splitExperimentsRandomness(storageService: IStorageService): IExperiments { - const random1 = getExperimentsRandomness(storageService); - const [/* random2 */, /* ripgrepQuickSearch */] = splitRandom(random1); - // const [/* random3 */, /* deployToAzureQuickLink */] = splitRandom(random2); - // const [random4, /* mergeQuickLinks */] = splitRandom(random3); - // const [random5, /* enableWelcomePage */] = splitRandom(random4); - return { - // ripgrepQuickSearch, - }; -} - -function getExperimentsRandomness(storageService: IStorageService) { - const key = 'experiments.randomness'; - let valueString = storageService.get(key); - if (!valueString) { - valueString = Math.random().toString(); - storageService.store(key, valueString); - } - - return parseFloat(valueString); -} - -function splitRandom(random: number): [number, boolean] { - const scaled = random * 2; - const i = Math.floor(scaled); - return [scaled - i, i === 1]; -} - -function getExperimentsOverrides(configurationService: IConfigurationService): IExperiments { - return deepClone(configurationService.getValue('experiments')) || {}; -} diff --git a/src/vs/platform/telemetry/common/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts deleted file mode 100644 index df3b884b3ed..00000000000 --- a/src/vs/platform/telemetry/common/telemetryIpc.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; - -export interface ITelemetryLog { - eventName: string; - data?: any; -} - -export interface ITelemetryAppenderChannel extends IChannel { - call(command: 'log', data: ITelemetryLog): TPromise; - call(command: string, arg: any): TPromise; -} - -export class TelemetryAppenderChannel implements ITelemetryAppenderChannel { - - constructor(private appender: ITelemetryAppender) { } - - call(command: string, { eventName, data }: ITelemetryLog): TPromise { - this.appender.log(eventName, data); - return TPromise.as(null); - } -} - -export class TelemetryAppenderClient implements ITelemetryAppender { - - constructor(private channel: ITelemetryAppenderChannel) { } - - log(eventName: string, data?: any): any { - this.channel.call('log', { eventName, data }) - .done(null, err => `Failed to log telemetry: ${console.warn(err)}`); - - return TPromise.as(null); - } - - dispose(): any { - // TODO - } -} \ No newline at end of file diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index dba27cde4b1..72b7d856930 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -21,7 +21,6 @@ export interface ITelemetryServiceConfig { appender: ITelemetryAppender; commonProperties?: TPromise<{ [name: string]: any }>; piiPaths?: string[]; - userOptIn?: boolean; } export class TelemetryService implements ITelemetryService { @@ -46,7 +45,7 @@ export class TelemetryService implements ITelemetryService { this._appender = config.appender; this._commonProperties = config.commonProperties || TPromise.as({}); this._piiPaths = config.piiPaths || []; - this._userOptIn = typeof config.userOptIn === 'undefined' ? true : config.userOptIn; + this._userOptIn = true; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi]; @@ -167,8 +166,9 @@ Registry.as(Extensions.Configuration).registerConfigurat 'properties': { 'telemetry.enableTelemetry': { 'type': 'boolean', - 'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to Microsoft."), - 'default': true + 'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to a Microsoft online service."), + 'default': true, + 'tags': ['usesOnlineServices'] } } }); \ No newline at end of file diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 60edb06869e..a098c7723c1 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -8,10 +8,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import { guessMimeTypes } from 'vs/base/common/mime'; import * as paths from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService, ConfigurationTarget, ConfigurationTargetToString } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ILogService } from 'vs/platform/log/common/log'; export const NullTelemetryService = new class implements ITelemetryService { _serviceBrand: undefined; @@ -30,13 +31,38 @@ export const NullTelemetryService = new class implements ITelemetryService { export interface ITelemetryAppender { log(eventName: string, data: any): void; + dispose(): TPromise; } export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender { - return { log: (e, d) => appenders.forEach(a => a.log(e, d)) }; + return { + log: (e, d) => appenders.forEach(a => a.log(e, d)), + dispose: () => TPromise.join(appenders.map(a => a.dispose())) + }; } -export const NullAppender: ITelemetryAppender = { log: () => null }; +export const NullAppender: ITelemetryAppender = { log: () => null, dispose: () => TPromise.as(null) }; + + +export class LogAppender implements ITelemetryAppender { + + private commonPropertiesRegex = /^sessionID$|^version$|^timestamp$|^commitHash$|^common\./; + constructor(@ILogService private readonly _logService: ILogService) { } + + dispose(): TPromise { + return TPromise.as(undefined); + } + + log(eventName: string, data: any): void { + const strippedData = {}; + Object.keys(data).forEach(key => { + if (!this.commonPropertiesRegex.test(key)) { + strippedData[key] = data[key]; + } + }); + this._logService.trace(`telemetry/${eventName}`, strippedData); + } +} /* __GDPR__FRAGMENT__ "URIDescriptor" : { @@ -87,8 +113,11 @@ const configurationValueWhitelist = [ 'editor.multiCursorModifier', 'editor.quickSuggestions', 'editor.quickSuggestionsDelay', - 'editor.parameterHints', + 'editor.parameterHints.enabled', + 'editor.parameterHints.cycle', 'editor.autoClosingBrackets', + 'editor.autoClosingQuotes', + 'editor.autoSurround', 'editor.autoIndent', 'editor.formatOnType', 'editor.formatOnPaste', @@ -126,35 +155,39 @@ const configurationValueWhitelist = [ 'editor.formatOnSave', 'editor.colorDecorators', - 'window.zoomLevel', - 'files.autoSave', - 'files.hotExit', + 'breadcrumbs.enabled', + 'breadcrumbs.filePath', + 'breadcrumbs.symbolPath', + 'breadcrumbs.useQuickPick', + 'explorer.openEditors.visible', + 'extensions.autoUpdate', 'files.associations', - 'workbench.statusBar.visible', + 'files.autoGuessEncoding', + 'files.autoSave', + 'files.autoSaveDelay', + 'files.encoding', + 'files.eol', + 'files.hotExit', 'files.trimTrailingWhitespace', 'git.confirmSync', - 'workbench.sideBar.location', - 'window.openFilesInNewWindow', - 'javascript.validate.enable', - 'window.restoreWindows', - 'extensions.autoUpdate', - 'files.eol', - 'explorer.openEditors.visible', - 'workbench.editor.enablePreview', - 'files.autoSaveDelay', - 'workbench.editor.showTabs', - 'files.encoding', - 'files.autoGuessEncoding', 'git.enabled', 'http.proxyStrictSSL', - 'terminal.integrated.fontFamily', - 'workbench.editor.enablePreviewFromQuickOpen', - 'workbench.editor.swipeToNavigate', + 'javascript.validate.enable', 'php.builtInCompletions.enable', 'php.validate.enable', 'php.validate.run', - 'workbench.welcome.enabled', + 'terminal.integrated.fontFamily', + 'window.openFilesInNewWindow', + 'window.restoreWindows', + 'window.zoomLevel', + 'workbench.editor.enablePreview', + 'workbench.editor.enablePreviewFromQuickOpen', + 'workbench.editor.showTabs', + 'workbench.editor.swipeToNavigate', + 'workbench.sideBar.location', 'workbench.startupEditor', + 'workbench.statusBar.visible', + 'workbench.welcome.enabled', ]; export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable { @@ -167,7 +200,7 @@ export function configurationTelemetry(telemetryService: ITelemetryService, conf } */ telemetryService.publicLog('updateConfiguration', { - configurationSource: ConfigurationTarget[event.source], + configurationSource: ConfigurationTargetToString(event.source), configurationKeys: flattenKeys(event.sourceConfig) }); /* __GDPR__ @@ -177,7 +210,7 @@ export function configurationTelemetry(telemetryService: ITelemetryService, conf } */ telemetryService.publicLog('updateConfigurationValues', { - configurationSource: ConfigurationTarget[event.source], + configurationSource: ConfigurationTargetToString(event.source), configurationValues: flattenValues(event.sourceConfig, configurationValueWhitelist) }); } diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 898f5bfc02c..e8a7ead4570 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -9,6 +9,7 @@ import { isObject } from 'vs/base/common/types'; import { safeStringify, mixin } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ILogService } from 'vs/platform/log/common/log'; let _initialized = false; @@ -53,7 +54,8 @@ export class AppInsightsAppender implements ITelemetryAppender { constructor( private _eventPrefix: string, private _defaultData: { [key: string]: any }, - aiKeyOrClientFactory: string | (() => typeof appInsights.client) // allow factory function for testing + aiKeyOrClientFactory: string | (() => typeof appInsights.client), // allow factory function for testing + @ILogService private _logService?: ILogService ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -133,8 +135,12 @@ export class AppInsightsAppender implements ITelemetryAppender { return; } data = mixin(data, this._defaultData); - let { properties, measurements } = AppInsightsAppender._getData(data); - this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, properties, measurements); + data = AppInsightsAppender._getData(data); + + if (this._logService) { + this._logService.trace(`telemetry/${eventName}`, data); + } + this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, data.properties, data.measurements); } dispose(): TPromise { diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/node/commonProperties.ts index e997435d9c1..d02cd24f2de 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/node/commonProperties.ts @@ -22,7 +22,7 @@ export function resolveCommonProperties(commit: string, version: string, machine // __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); // __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platform'] = Platform.Platform[Platform.platform]; + result['common.platform'] = Platform.PlatformToString(Platform.platform); // __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.nodePlatform'] = process.platform; // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } diff --git a/src/vs/platform/telemetry/node/telemetryIpc.ts b/src/vs/platform/telemetry/node/telemetryIpc.ts new file mode 100644 index 00000000000..bbe1c20c9c6 --- /dev/null +++ b/src/vs/platform/telemetry/node/telemetryIpc.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { Event } from 'vs/base/common/event'; + +export interface ITelemetryLog { + eventName: string; + data?: any; +} + +export interface ITelemetryAppenderChannel extends IChannel { + call(command: 'log', data: ITelemetryLog): Thenable; + call(command: string, arg: any): Thenable; +} + +export class TelemetryAppenderChannel implements ITelemetryAppenderChannel { + + constructor(private appender: ITelemetryAppender) { } + + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + + call(command: string, { eventName, data }: ITelemetryLog): Thenable { + this.appender.log(eventName, data); + return TPromise.as(null); + } +} + +export class TelemetryAppenderClient implements ITelemetryAppender { + + constructor(private channel: ITelemetryAppenderChannel) { } + + log(eventName: string, data?: any): any { + this.channel.call('log', { eventName, data }) + .then(null, err => `Failed to log telemetry: ${console.warn(err)}`); + + return TPromise.as(null); + } + + dispose(): any { + // TODO + } +} \ No newline at end of file diff --git a/src/vs/platform/telemetry/node/telemetryNodeUtils.ts b/src/vs/platform/telemetry/node/telemetryNodeUtils.ts index 9f65136e10b..4e8e33d1800 100644 --- a/src/vs/platform/telemetry/node/telemetryNodeUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryNodeUtils.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/node/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts index a298f26aa91..040a51185f9 100644 --- a/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/appInsightsAppender.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { ILogService, AbstractLogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; interface IAppInsightsEvent { eventName: string; @@ -39,11 +40,61 @@ class AppInsightsMock { } } +class TestableLogService extends AbstractLogService implements ILogService { + _serviceBrand: any; + + public logs: string[] = []; + + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { + super(); + this.setLevel(logLevel); + } + + trace(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + this.logs.push(message + JSON.stringify(args)); + } + } + + debug(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + this.logs.push(message); + } + } + + info(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + this.logs.push(message); + } + } + + warn(message: string | Error, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + this.logs.push(message.toString()); + } + } + + error(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + this.logs.push(message); + } + } + + critical(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + this.logs.push(message); + } + } + + dispose(): void { } +} + suite('AIAdapter', () => { var appInsightsMock: AppInsightsMock; var adapter: AppInsightsAppender; var prefix = 'prefix'; + setup(() => { appInsightsMock = new AppInsightsMock(); adapter = new AppInsightsAppender(prefix, undefined, () => appInsightsMock); @@ -141,4 +192,27 @@ suite('AIAdapter', () => { assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({ 'testProperty': 'test' })); assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'], 1); }); + + test('Do not Log Telemetry if log level is not trace', () => { + const logService = new TestableLogService(LogLevel.Info); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 0); + }); + + test('Log Telemetry if log level is trace', () => { + const logService = new TestableLogService(LogLevel.Trace); + adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService); + adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 }); + assert.equal(logService.logs.length, 1); + assert.equal(logService.logs[0], 'telemetry/testEvent' + JSON.stringify([{ + properties: { + hello: 'world', + 'common.platform': 'Windows' + }, + measurements: { + isTrue: 1, numberBetween1And3: 2 + } + }])); + }); }); \ No newline at end of file diff --git a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts index d0b935f401d..8cd91f0803b 100644 --- a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts @@ -14,8 +14,6 @@ import * as Errors from 'vs/base/common/errors'; import * as sinon from 'sinon'; import { getConfigurationValue } from 'vs/platform/configuration/common/configuration'; -const optInStatusEventName: string = 'optInStatus'; - class TestTelemetryAppender implements ITelemetryAppender { public events: any[]; @@ -34,8 +32,9 @@ class TestTelemetryAppender implements ITelemetryAppender { return this.events.length; } - public dispose() { + public dispose(): TPromise { this.isDisposed = true; + return TPromise.as(null); } } @@ -718,29 +717,9 @@ suite('TelemetryService', () => { } })); - test('Telemetry Service respects user opt-in settings', sinon.test(function () { + test('Telemetry Service sends events when enableTelemetry is on', sinon.test(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined); - - return service.publicLog('testEvent').then(() => { - assert.equal(testAppender.getEventsCount(), 0); - service.dispose(); - }); - })); - - test('Telemetry Service does not sent optInStatus when user opted out', sinon.test(function () { - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined); - - return service.publicLog(optInStatusEventName, { optIn: false }).then(() => { - assert.equal(testAppender.getEventsCount(), 0); - service.dispose(); - }); - })); - - test('Telemetry Service sends events when enableTelemetry is on even user optin is on', sinon.test(function () { - let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ userOptIn: true, appender: testAppender }, undefined); + let service = new TelemetryService({ appender: testAppender }, undefined); return service.publicLog('testEvent').then(() => { assert.equal(testAppender.getEventsCount(), 1); diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 241993fb8f8..9af7371f18a 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -149,11 +149,11 @@ export function getColorRegistry(): IColorRegistry { // ----- base colors -export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#6C6C6C', hc: '#FFFFFF' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); +export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hc: '#FFFFFF' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hc: '#F48771' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); -export const descriptionForeground = registerColor('descriptionForeground', { light: transparent(foreground, 0.7), dark: transparent(foreground, 0.7), hc: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); +export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hc: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); -export const focusBorder = registerColor('focusBorder', { dark: Color.fromHex('#0E639C').transparent(0.6), light: Color.fromHex('#007ACC').transparent(0.4), hc: '#F38518' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); +export const focusBorder = registerColor('focusBorder', { dark: Color.fromHex('#0E639C').transparent(0.8), light: Color.fromHex('#007ACC').transparent(0.4), hc: '#F38518' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); export const contrastBorder = registerColor('contrastBorder', { light: null, dark: null, hc: '#6FC3DF' }, nls.localize('contrastBorder', "An extra border around elements to separate them from others for greater contrast.")); export const activeContrastBorder = registerColor('contrastActiveBorder', { light: null, dark: null, hc: focusBorder }, nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); @@ -163,8 +163,8 @@ export const selectionBackground = registerColor('selection.background', { light // ------ text colors export const textSeparatorForeground = registerColor('textSeparator.foreground', { light: '#0000002e', dark: '#ffffff2e', hc: Color.black }, nls.localize('textSeparatorForeground', "Color for text separators.")); -export const textLinkForeground = registerColor('textLink.foreground', { light: '#4080D0', dark: '#4080D0', hc: '#4080D0' }, nls.localize('textLinkForeground', "Foreground color for links in text.")); -export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#4080D0', dark: '#4080D0', hc: '#4080D0' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); +export const textLinkForeground = registerColor('textLink.foreground', { light: '#006AB1', dark: '#3794FF', hc: '#3794FF' }, nls.localize('textLinkForeground', "Foreground color for links in text.")); +export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#006AB1', dark: '#3794FF', hc: '#3794FF' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); export const textPreformatForeground = registerColor('textPreformat.foreground', { light: '#A31515', dark: '#D7BA7D', hc: '#D7BA7D' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); export const textBlockQuoteBackground = registerColor('textBlockQuote.background', { light: '#7f7f7f1a', dark: '#7f7f7f1a', hc: null }, nls.localize('textBlockQuoteBackground', "Background color for block quotes in text.")); export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { light: '#007acc80', dark: '#007acc80', hc: Color.white }, nls.localize('textBlockQuoteBorder', "Border color for block quotes in text.")); @@ -177,13 +177,16 @@ export const inputBackground = registerColor('input.background', { dark: '#3C3C3 export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground.")); export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border.")); export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC', light: '#007ACC', hc: activeContrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); -export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { dark: null, light: null, hc: null }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); +export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); +export const inputValidationInfoForeground = registerColor('inputValidation.infoForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationInfoForeground', "Input validation foreground color for information severity.")); export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hc: Color.black }, nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); +export const inputValidationWarningForeground = registerColor('inputValidation.warningForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationWarningForeground', "Input validation foreground color for warning severity.")); export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', { dark: '#B89500', light: '#B89500', hc: contrastBorder }, nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', { dark: '#5A1D1D', light: '#F2DEDE', hc: Color.black }, nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); +export const inputValidationErrorForeground = registerColor('inputValidation.errorForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationErrorForeground', "Input validation foreground color for error severity.")); export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', { dark: '#BE1100', light: '#BE1100', hc: contrastBorder }, nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); export const selectBackground = registerColor('dropdown.background', { dark: '#3C3C3C', light: Color.white, hc: Color.black }, nls.localize('dropdownBackground', "Dropdown background.")); @@ -191,30 +194,30 @@ export const selectListBackground = registerColor('dropdown.listBackground', { d export const selectForeground = registerColor('dropdown.foreground', { dark: '#F0F0F0', light: null, hc: Color.white }, nls.localize('dropdownForeground', "Dropdown foreground.")); export const selectBorder = registerColor('dropdown.border', { dark: selectBackground, light: '#CECECE', hc: contrastBorder }, nls.localize('dropdownBorder', "Dropdown border.")); -export const listFocusBackground = registerColor('list.focusBackground', { dark: '#073655', light: '#DCEBFC', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listFocusBackground = registerColor('list.focusBackground', { dark: '#062F4A', light: '#DFF0FF', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#3399FF', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#2477CE', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#3F3F46', light: '#CCCEDB', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#dddfea', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: listFocusBackground, light: listFocusBackground, hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#0097fb', light: '#007acc', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hc: '#B89500' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); -export const listErrorForeground = registerColor('list.errorForeground', { dark: '#ea4646', light: '#d60a0a', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); +export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); export const listWarningForeground = registerColor('list.warningForeground', { dark: '#4d9e4d', light: '#117711', hc: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); -export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: Color.fromHex('#0097FB').transparent(0.6), light: Color.fromHex('#007ACC').transparent(0.6), hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); +export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#006AB1', hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hc: Color.white }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hc: null }, nls.localize('buttonBackground', "Button background color.")); export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hc: null }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); -export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#BEBEBE', hc: Color.black }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); -export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); +export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hc: Color.black }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); +export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hc: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hc: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hc: transparent(contrastBorder, 0.6) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); @@ -287,6 +290,17 @@ export const diffRemoved = registerColor('diffEditor.removedTextBackground', { d export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hc: '#33ff2eff' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hc: '#FF008F' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); +export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.')); + +/** + * Breadcrumb colors + */ +export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: transparent(foreground, .8), dark: transparent(foreground, .8), hc: transparent(foreground, .8) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsBackground = registerColor('breadcrumb.background', { light: editorBackground, dark: editorBackground, hc: editorBackground }, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); +export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: darken(foreground, .2), dark: lighten(foreground, .1), hc: lighten(foreground, .1) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, .2), dark: lighten(foreground, .1), hc: lighten(foreground, .1) }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); +export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); + /** * Merge-conflict colors */ diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 07dfd1d9551..c53e9f1c41b 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,7 +6,7 @@ 'use strict'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -89,10 +89,13 @@ export interface IInputBoxStyleOverrides extends IStyleOverrides { inputActiveOptionBorder?: ColorIdentifier; inputValidationInfoBorder?: ColorIdentifier; inputValidationInfoBackground?: ColorIdentifier; + inputValidationInfoForeground?: ColorIdentifier; inputValidationWarningBorder?: ColorIdentifier; inputValidationWarningBackground?: ColorIdentifier; + inputValidationWarningForeground?: ColorIdentifier; inputValidationErrorBorder?: ColorIdentifier; inputValidationErrorBackground?: ColorIdentifier; + inputValidationErrorForeground?: ColorIdentifier; } export function attachInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInputBoxStyleOverrides): IDisposable { @@ -102,10 +105,13 @@ export function attachInputBoxStyler(widget: IThemable, themeService: IThemeServ inputBorder: (style && style.inputBorder) || inputBorder, inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || inputValidationInfoBorder, inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || inputValidationInfoBackground, + inputValidationInfoForeground: (style && style.inputValidationInfoForeground) || inputValidationInfoForeground, inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || inputValidationWarningBorder, inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || inputValidationWarningBackground, + inputValidationWarningForeground: (style && style.inputValidationWarningForeground) || inputValidationWarningForeground, inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || inputValidationErrorBorder, - inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground + inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground, + inputValidationErrorForeground: (style && style.inputValidationErrorForeground) || inputValidationErrorForeground } as IInputBoxStyleOverrides, widget); } @@ -129,7 +135,8 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer listFocusOutline: (style && style.listFocusOutline) || activeContrastBorder, listHoverBackground: (style && style.listHoverBackground) || listHoverBackground, listHoverForeground: (style && style.listHoverForeground) || listHoverForeground, - listHoverOutline: (style && style.listFocusOutline) || activeContrastBorder + listHoverOutline: (style && style.listFocusOutline) || activeContrastBorder, + selectListBorder: (style && style.selectListBorder) || editorWidgetBorder } as ISelectBoxStyleOverrides, widget); } @@ -141,10 +148,13 @@ export function attachFindInputBoxStyler(widget: IThemable, themeService: ITheme inputActiveOptionBorder: (style && style.inputActiveOptionBorder) || inputActiveOptionBorder, inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || inputValidationInfoBorder, inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || inputValidationInfoBackground, + inputValidationInfoForeground: (style && style.inputValidationInfoForeground) || inputValidationInfoForeground, inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || inputValidationWarningBorder, inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || inputValidationWarningBackground, + inputValidationWarningForeground: (style && style.inputValidationWarningForeground) || inputValidationWarningForeground, inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || inputValidationErrorBorder, - inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground + inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground, + inputValidationErrorForeground: (style && style.inputValidationErrorForeground) || inputValidationErrorForeground } as IInputBoxStyleOverrides, widget); } @@ -171,13 +181,16 @@ export function attachQuickOpenStyler(widget: IThemable, themeService: IThemeSer inputBorder: (style && style.inputBorder) || inputBorder, inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || inputValidationInfoBorder, inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || inputValidationInfoBackground, + inputValidationInfoForeground: (style && style.inputValidationInfoForeground) || inputValidationInfoForeground, inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || inputValidationWarningBorder, inputValidationWarningBackground: (style && style.inputValidationWarningBackground) || inputValidationWarningBackground, + inputValidationWarningForeground: (style && style.inputValidationWarningForeground) || inputValidationWarningForeground, inputValidationErrorBorder: (style && style.inputValidationErrorBorder) || inputValidationErrorBorder, inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground, + inputValidationErrorForeground: (style && style.inputValidationErrorForeground) || inputValidationErrorForeground, listFocusBackground: (style && style.listFocusBackground) || listFocusBackground, listFocusForeground: (style && style.listFocusForeground) || listFocusForeground, - listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || lighten(listActiveSelectionBackground, 0.1), + listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || listActiveSelectionBackground, listActiveSelectionForeground: (style && style.listActiveSelectionForeground) || listActiveSelectionForeground, listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || listActiveSelectionBackground, listFocusAndSelectionForeground: (style && style.listFocusAndSelectionForeground) || listActiveSelectionForeground, @@ -219,7 +232,7 @@ export function attachListStyler(widget: IThemable, themeService: IThemeService, export const defaultListStyles: IColorMapping = { listFocusBackground: listFocusBackground, listFocusForeground: listFocusForeground, - listActiveSelectionBackground: lighten(listActiveSelectionBackground, 0.1), + listActiveSelectionBackground: listActiveSelectionBackground, listActiveSelectionForeground: listActiveSelectionForeground, listFocusAndSelectionBackground: listActiveSelectionBackground, listFocusAndSelectionForeground: listActiveSelectionForeground, @@ -261,4 +274,24 @@ export function attachProgressBarStyler(widget: IThemable, themeService: IThemeS export function attachStylerCallback(themeService: IThemeService, colors: { [name: string]: ColorIdentifier }, callback: styleFn): IDisposable { return attachStyler(themeService, colors, callback); -} \ No newline at end of file +} + +export interface IBreadcrumbsWidgetStyleOverrides extends IColorMapping { + breadcrumbsBackground?: ColorIdentifier | ColorFunction; + breadcrumbsForeground?: ColorIdentifier; + breadcrumbsHoverForeground?: ColorIdentifier; + breadcrumbsFocusForeground?: ColorIdentifier; + breadcrumbsFocusAndSelectionForeground?: ColorIdentifier; +} + +export const defaultBreadcrumbsStyles = { + breadcrumbsBackground: breadcrumbsBackground, + breadcrumbsForeground: breadcrumbsForeground, + breadcrumbsHoverForeground: breadcrumbsFocusForeground, + breadcrumbsFocusForeground: breadcrumbsFocusForeground, + breadcrumbsFocusAndSelectionForeground: breadcrumbsActiveSelectionForeground, +}; + +export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultBreadcrumbsStyles, ...style }, widget); +} diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index c58b79603bc..7c1e2f9bae6 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -6,7 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Color } from 'vs/base/common/color'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/platform/registry/common/platform'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; @@ -111,12 +111,10 @@ class ThemingRegistry implements IThemingRegistry { public onThemeChange(participant: IThemingParticipant): IDisposable { this.themingParticipants.push(participant); this.onThemingParticipantAddedEmitter.fire(participant); - return { - dispose: () => { - const idx = this.themingParticipants.indexOf(participant); - this.themingParticipants.splice(idx, 1); - } - }; + return toDisposable(() => { + const idx = this.themingParticipants.indexOf(participant); + this.themingParticipants.splice(idx, 1); + }); } public get onThemingParticipantAdded(): Event { diff --git a/src/vs/platform/theme/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/platform/theme/test/electron-browser/colorRegistry.releaseTest.ts index 39858929cb5..01335f98f8e 100644 --- a/src/vs/platform/theme/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/platform/theme/test/electron-browser/colorRegistry.releaseTest.ts @@ -18,6 +18,8 @@ import { request, asText } from 'vs/base/node/request'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'path'; import * as assert from 'assert'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { CancellationToken } from 'vs/base/common/cancellation'; interface ColorInfo { @@ -40,7 +42,7 @@ export const experimental = []; // 'settings.modifiedItemForeground', 'editorUnn suite('Color Registry', function () { test('all colors documented', async function () { - const reqContext = await request({ url: 'https://raw.githubusercontent.com/Microsoft/vscode-docs/vnext/docs/getstarted/theme-color-reference.md' }); + const reqContext = await request({ url: 'https://raw.githubusercontent.com/Microsoft/vscode-docs/vnext/docs/getstarted/theme-color-reference.md' }, CancellationToken.None); const content = await asText(reqContext); const expression = /\-\s*\`([\w\.]+)\`: (.*)/g; @@ -103,7 +105,7 @@ function getDescription(color: ColorContribution) { } async function getColorsFromExtension(): Promise<{ [id: string]: string }> { - let extPath = require.toUrl('../../../../../../extensions'); + let extPath = getPathFromAmdModule(require, '../../../../../../extensions'); let extFolders = await pfs.readDirsInDir(extPath); let result: { [id: string]: string } = Object.create(null); for (let folder of extFolders) { @@ -127,4 +129,4 @@ async function getColorsFromExtension(): Promise<{ [id: string]: string }> { } return result; -} \ No newline at end of file +} diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index fbaebf81a99..62c53443088 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -37,7 +37,7 @@ export interface IUpdate { * Donwloaded: There is an update ready to be installed in the background (win32). */ -export enum StateType { +export const enum StateType { Uninitialized = 'uninitialized', Idle = 'idle', CheckingForUpdates = 'checking for updates', @@ -48,8 +48,13 @@ export enum StateType { Ready = 'ready', } +export const enum UpdateType { + Setup, + Archive +} + export type Uninitialized = { type: StateType.Uninitialized }; -export type Idle = { type: StateType.Idle }; +export type Idle = { type: StateType.Idle, updateType: UpdateType }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates, context: any }; export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate }; export type Downloading = { type: StateType.Downloading, update: IUpdate }; @@ -61,7 +66,7 @@ export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDown export const State = { Uninitialized: { type: StateType.Uninitialized } as Uninitialized, - Idle: { type: StateType.Idle } as Idle, + Idle: (updateType: UpdateType) => ({ type: StateType.Idle, updateType }) as Idle, CheckingForUpdates: (context: any) => ({ type: StateType.CheckingForUpdates, context } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), @@ -89,4 +94,6 @@ export interface IUpdateService { downloadUpdate(): TPromise; applyUpdate(): TPromise; quitAndInstall(): TPromise; -} \ No newline at end of file + + isLatestVersion(): TPromise; +} diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 6734f82fa36..218a14ee8ae 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -6,14 +6,16 @@ 'use strict'; import { Event, Emitter } from 'vs/base/common/event'; -import { Throttler } from 'vs/base/common/async'; +import { Throttler, timeout } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import product from 'vs/platform/node/product'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, StateType, AvailableForDownload } from 'vs/platform/update/common/update'; +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'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { CancellationToken } from 'vs/base/common/cancellation'; export function createUpdateURL(platform: string, quality: string): string { return `${product.updateUrl}/api/update/${platform}/${quality}/${product.commit}`; @@ -23,6 +25,8 @@ export abstract class AbstractUpdateService implements IUpdateService { _serviceBrand: any; + protected readonly url: string | undefined; + private _state: State = State.Uninitialized; private throttler: Throttler = new Throttler(); @@ -43,7 +47,8 @@ export abstract class AbstractUpdateService implements IUpdateService { @ILifecycleService private lifecycleService: ILifecycleService, @IConfigurationService protected configurationService: IConfigurationService, @IEnvironmentService private environmentService: IEnvironmentService, - @ILogService protected logService: ILogService + @IRequestService protected requestService: IRequestService, + @ILogService protected logService: ILogService, ) { if (this.environmentService.disableUpdates) { this.logService.info('update#ctor - updates are disabled'); @@ -62,16 +67,16 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - if (!this.setUpdateFeedUrl(quality)) { + this.url = this.buildUpdateFeedUrl(quality); + if (!this.url) { this.logService.info('update#ctor - updates are disabled'); return; } - this.setState({ type: StateType.Idle }); + this.setState(State.Idle(this.getUpdateType())); // Start checking for updates after 30 seconds - this.scheduleCheckForUpdates(30 * 1000) - .done(null, err => this.logService.error(err)); + this.scheduleCheckForUpdates(30 * 1000).then(null, err => this.logService.error(err)); } private getProductQuality(): string { @@ -79,8 +84,8 @@ export abstract class AbstractUpdateService implements IUpdateService { return quality === 'none' ? null : product.quality; } - private scheduleCheckForUpdates(delay = 60 * 60 * 1000): TPromise { - return TPromise.timeout(delay) + private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Thenable { + return timeout(delay) .then(() => this.checkForUpdates(null)) .then(update => { if (update) { @@ -140,7 +145,7 @@ export abstract class AbstractUpdateService implements IUpdateService { this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - this.lifecycleService.quit(true /* from update */).done(vetod => { + this.lifecycleService.quit(true /* from update */).then(vetod => { this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; @@ -153,10 +158,29 @@ export abstract class AbstractUpdateService implements IUpdateService { return TPromise.as(null); } + isLatestVersion(): TPromise { + if (!this.url) { + return TPromise.as(undefined); + } + return this.requestService.request({ url: this.url }, CancellationToken.None).then(context => { + // The update server replies with 204 (No Content) when no + // update is available - that's all we want to know. + if (context.res.statusCode === 204) { + return true; + } else { + return false; + } + }); + } + + protected getUpdateType(): UpdateType { + return UpdateType.Archive; + } + protected doQuitAndInstall(): void { // noop } - protected abstract setUpdateFeedUrl(quality: string): boolean; + protected abstract buildUpdateFeedUrl(quality: string): string | undefined; protected abstract doCheckForUpdates(context: any): void; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index a75576d9195..8ac8357ef63 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -11,11 +11,12 @@ import { Event, fromNodeEventEmitter } from 'vs/base/common/event'; import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { State, IUpdate, StateType, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractUpdateService, createUpdateURL } from 'vs/platform/update/electron-main/abstractUpdateService'; +import { IRequestService } from 'vs/platform/request/node/request'; export class DarwinUpdateService extends AbstractUpdateService { @@ -33,9 +34,10 @@ export class DarwinUpdateService extends AbstractUpdateService { @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private telemetryService: ITelemetryService, @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService requestService: IRequestService, @ILogService logService: ILogService ) { - super(lifecycleService, configurationService, environmentService, logService); + super(lifecycleService, configurationService, environmentService, requestService, logService); this.onRawError(this.onError, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); @@ -44,19 +46,19 @@ export class DarwinUpdateService extends AbstractUpdateService { private onError(err: string): void { this.logService.error('UpdateService error: ', err); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } - protected setUpdateFeedUrl(quality: string): boolean { + protected buildUpdateFeedUrl(quality: string): string | undefined { + const url = createUpdateURL('darwin', quality); try { - electron.autoUpdater.setFeedURL(createUpdateURL('darwin', quality)); + electron.autoUpdater.setFeedURL({ url }); } catch (e) { // application is very likely not signed this.logService.error('Failed to set update feed URL', e); - return false; + return undefined; } - - return true; + return url; } protected doCheckForUpdates(context: any): void { @@ -99,7 +101,7 @@ export class DarwinUpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!this.state.context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } protected doQuitAndInstall(): void { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 12d660bb203..cce8a725374 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -9,7 +9,7 @@ import product from 'vs/platform/node/product'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IRequestService } from 'vs/platform/request/node/request'; -import { State, IUpdate, AvailableForDownload } from 'vs/platform/update/common/update'; +import { State, IUpdate, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,27 +17,25 @@ import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/elect import { asJson } from 'vs/base/node/request'; import { TPromise } from 'vs/base/common/winjs.base'; import { shell } from 'electron'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class LinuxUpdateService extends AbstractUpdateService { _serviceBrand: any; - private url: string | undefined; - constructor( @ILifecycleService lifecycleService: ILifecycleService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private telemetryService: ITelemetryService, @IEnvironmentService environmentService: IEnvironmentService, - @IRequestService private requestService: IRequestService, + @IRequestService requestService: IRequestService, @ILogService logService: ILogService ) { - super(lifecycleService, configurationService, environmentService, logService); + super(lifecycleService, configurationService, environmentService, requestService, logService); } - protected setUpdateFeedUrl(quality: string): boolean { - this.url = createUpdateURL(`linux-${process.arch}`, quality); - return true; + protected buildUpdateFeedUrl(quality: string): string { + return createUpdateURL(`linux-${process.arch}`, quality); } protected doCheckForUpdates(context: any): void { @@ -47,7 +45,7 @@ export class LinuxUpdateService extends AbstractUpdateService { this.setState(State.CheckingForUpdates(context)); - this.requestService.request({ url: this.url }) + this.requestService.request({ url: this.url }, CancellationToken.None) .then(asJson) .then(update => { if (!update || !update.url || !update.version || !update.productVersion) { @@ -58,7 +56,7 @@ export class LinuxUpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); } else { this.setState(State.AvailableForDownload(update)); } @@ -72,7 +70,7 @@ export class LinuxUpdateService extends AbstractUpdateService { } */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); }); } @@ -84,8 +82,8 @@ export class LinuxUpdateService extends AbstractUpdateService { } else { shell.openExternal(state.update.url); } - this.setState(State.Idle); + this.setState(State.Idle(UpdateType.Archive)); return TPromise.as(null); } } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 220e80a782a..3d1772cfe97 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -5,7 +5,7 @@ 'use strict'; -import * as fs from 'original-fs'; +import * as fs from 'fs'; import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; import { memoize } from 'vs/base/common/decorators'; @@ -14,7 +14,7 @@ import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycle import { IRequestService } from 'vs/platform/request/node/request'; import product from 'vs/platform/node/product'; import { TPromise, Promise } from 'vs/base/common/winjs.base'; -import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { State, IUpdate, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; @@ -23,6 +23,8 @@ import { download, asJson } from 'vs/base/node/request'; import { checksum } from 'vs/base/node/crypto'; import { tmpdir } from 'os'; import { spawn } from 'child_process'; +import { shell } from 'electron'; +import { CancellationToken } from 'vs/base/common/cancellation'; function pollUntil(fn: () => boolean, timeout = 1000): TPromise { return new TPromise(c => { @@ -43,16 +45,26 @@ interface IAvailableUpdate { updateFilePath?: string; } +let _updateType: UpdateType | undefined = undefined; +function getUpdateType(): UpdateType { + if (typeof _updateType === 'undefined') { + _updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe')) + ? UpdateType.Setup + : UpdateType.Archive; + } + + return _updateType; +} + export class Win32UpdateService extends AbstractUpdateService { _serviceBrand: any; - private url: string | undefined; private availableUpdate: IAvailableUpdate | undefined; @memoize get cachePath(): TPromise { - const result = path.join(tmpdir(), `vscode-update-${process.arch}`); + const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`); return pfs.mkdirp(result, null).then(() => result); } @@ -61,19 +73,40 @@ export class Win32UpdateService extends AbstractUpdateService { @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private telemetryService: ITelemetryService, @IEnvironmentService environmentService: IEnvironmentService, - @IRequestService private requestService: IRequestService, + @IRequestService requestService: IRequestService, @ILogService logService: ILogService ) { - super(lifecycleService, configurationService, environmentService, logService); + super(lifecycleService, configurationService, environmentService, requestService, logService); + + if (getUpdateType() === UpdateType.Setup) { + /* __GDPR__ + "update:win32SetupTarget" : { + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + /* __GDPR__ + "update:winSetupTarget" : { + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + telemetryService.publicLog('update:win32SetupTarget', { target: product.target }); + } } - protected setUpdateFeedUrl(quality: string): boolean { - if (!fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { - return false; + protected buildUpdateFeedUrl(quality: string): string | undefined { + let platform = 'win32'; + + if (process.arch === 'x64') { + platform += '-x64'; } - this.url = createUpdateURL(process.arch === 'x64' ? 'win32-x64' : 'win32', quality); - return true; + if (getUpdateType() === UpdateType.Archive) { + platform += '-archive'; + } else if (product.target === 'user') { + platform += '-user'; + } + + return createUpdateURL(platform, quality); } protected doCheckForUpdates(context: any): void { @@ -83,10 +116,12 @@ export class Win32UpdateService extends AbstractUpdateService { this.setState(State.CheckingForUpdates(context)); - this.requestService.request({ url: this.url }) + this.requestService.request({ url: this.url }, CancellationToken.None) .then(asJson) .then(update => { - if (!update || !update.url || !update.version) { + const updateType = getUpdateType(); + + if (!update || !update.url || !update.version || !update.productVersion) { /* __GDPR__ "update:notAvailable" : { "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } @@ -94,7 +129,12 @@ export class Win32UpdateService extends AbstractUpdateService { */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(updateType)); + return TPromise.as(null); + } + + if (updateType === UpdateType.Archive) { + this.setState(State.AvailableForDownload(update)); return TPromise.as(null); } @@ -111,7 +151,7 @@ export class Win32UpdateService extends AbstractUpdateService { const hash = update.hash; const downloadPath = `${updatePackagePath}.tmp`; - return this.requestService.request({ url }) + return this.requestService.request({ url }, CancellationToken.None) .then(context => download(downloadPath, context)) .then(hash ? () => checksum(downloadPath, update.hash) : () => null) .then(() => pfs.rename(downloadPath, updatePackagePath)) @@ -123,7 +163,11 @@ export class Win32UpdateService extends AbstractUpdateService { this.availableUpdate = { packagePath }; if (fastUpdatesEnabled && update.supportsFastUpdate) { - this.setState(State.Downloaded(update)); + if (product.target === 'user') { + this.doApplyUpdate(); + } else { + this.setState(State.Downloaded(update)); + } } else { this.setState(State.Ready(update)); } @@ -138,10 +182,16 @@ export class Win32UpdateService extends AbstractUpdateService { } */ this.telemetryService.publicLog('update:notAvailable', { explicit: !!context }); - this.setState(State.Idle); + this.setState(State.Idle(getUpdateType())); }); } + protected doDownloadUpdate(state: AvailableForDownload): TPromise { + shell.openExternal(state.update.url); + this.setState(State.Idle(getUpdateType())); + return TPromise.as(null); + } + private getUpdatePackagePath(version: string): TPromise { return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`)); } @@ -159,7 +209,11 @@ export class Win32UpdateService extends AbstractUpdateService { } protected doApplyUpdate(): TPromise { - if (this.state.type !== StateType.Downloaded || !this.availableUpdate) { + if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) { + return TPromise.as(null); + } + + if (!this.availableUpdate) { return TPromise.as(null); } @@ -178,7 +232,7 @@ export class Win32UpdateService extends AbstractUpdateService { child.once('exit', () => { this.availableUpdate = undefined; - this.setState(State.Idle); + this.setState(State.Idle(getUpdateType())); }); const readyMutexName = `${product.win32MutexName}-ready`; @@ -207,4 +261,8 @@ export class Win32UpdateService extends AbstractUpdateService { }); } } + + protected getUpdateType(): UpdateType { + return getUpdateType(); + } } diff --git a/src/vs/platform/update/node/update.config.contribution.ts b/src/vs/platform/update/node/update.config.contribution.ts index 86ad52731dc..7ad60bdfc4f 100644 --- a/src/vs/platform/update/node/update.config.contribution.ts +++ b/src/vs/platform/update/node/update.config.contribution.ts @@ -21,18 +21,21 @@ configurationRegistry.registerConfiguration({ 'enum': ['none', 'default'], 'default': 'default', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change.") + 'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change. The updates are fetched from an online service."), + 'tags': ['usesOnlineServices'] }, 'update.enableWindowsBackgroundUpdates': { 'type': 'boolean', 'default': true, 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.") + 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates. The updates are fetched from an online service."), + 'tags': ['usesOnlineServices'] }, 'update.showReleaseNotes': { 'type': 'boolean', 'default': true, - 'description': nls.localize('showReleaseNotes', "Show Release Notes after an update.") + 'description': nls.localize('showReleaseNotes', "Show Release Notes after an update. The Release Notes are fetched from an online service."), + 'tags': ['usesOnlineServices'] } } }); diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/node/updateIpc.ts similarity index 75% rename from src/vs/platform/update/common/updateIpc.ts rename to src/vs/platform/update/node/updateIpc.ts index 2e26e13ed3d..42d5b2ecaec 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/node/updateIpc.ts @@ -6,17 +6,20 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; import { Event, Emitter } from 'vs/base/common/event'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IUpdateService, State } from './update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; export interface IUpdateChannel extends IChannel { + listen(event: 'onStateChange'): Event; + listen(command: string, arg?: any): Event; + call(command: 'checkForUpdates', arg: any): TPromise; call(command: 'downloadUpdate'): TPromise; call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; + call(command: 'isLatestVersion'): TPromise; call(command: string, arg?: any): TPromise; } @@ -24,14 +27,22 @@ export class UpdateChannel implements IUpdateChannel { constructor(private service: IUpdateService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'onStateChange': return this.service.onStateChange; + } + + throw new Error('No event found'); + } + call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); case 'downloadUpdate': return this.service.downloadUpdate(); case 'applyUpdate': return this.service.applyUpdate(); case 'quitAndInstall': return this.service.quitAndInstall(); case '_getInitialState': return TPromise.as(this.service.state); + case 'isLatestVersion': return this.service.isLatestVersion(); } return undefined; } @@ -41,8 +52,6 @@ export class UpdateChannelClient implements IUpdateService { _serviceBrand: any; - private _onRemoteStateChange = eventFromCall(this.channel, 'event:onStateChange'); - private _onStateChange = new Emitter(); get onStateChange(): Event { return this._onStateChange.event; } @@ -53,13 +62,14 @@ export class UpdateChannelClient implements IUpdateService { // always set this._state as the state changes this.onStateChange(state => this._state = state); - channel.call('_getInitialState').done(state => { + channel.call('_getInitialState').then(state => { // fire initial state this._onStateChange.fire(state); // fire subsequent states as they come in from remote - this._onRemoteStateChange(state => this._onStateChange.fire(state)); - }, onUnexpectedError); + + this.channel.listen('onStateChange')(state => this._onStateChange.fire(state)); + }); } checkForUpdates(context: any): TPromise { @@ -77,4 +87,8 @@ export class UpdateChannelClient implements IUpdateService { quitAndInstall(): TPromise { return this.channel.call('quitAndInstall'); } -} \ No newline at end of file + + isLatestVersion(): TPromise { + return this.channel.call('isLatestVersion'); + } +} diff --git a/src/vs/platform/url/common/url.ts b/src/vs/platform/url/common/url.ts index 63bfc01b6c4..2d1582c4465 100644 --- a/src/vs/platform/url/common/url.ts +++ b/src/vs/platform/url/common/url.ts @@ -5,7 +5,7 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; diff --git a/src/vs/platform/url/common/urlIpc.ts b/src/vs/platform/url/common/urlIpc.ts deleted file mode 100644 index 0cc9ec695ad..00000000000 --- a/src/vs/platform/url/common/urlIpc.ts +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IURLHandler, IURLService } from './url'; -import URI from 'vs/base/common/uri'; -import { IDisposable } from 'vs/base/common/lifecycle'; - -export interface IURLServiceChannel extends IChannel { - call(command: 'open', url: string): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class URLServiceChannel implements IURLServiceChannel { - - constructor(private service: IURLService) { } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'open': return this.service.open(URI.revive(arg)); - } - return undefined; - } -} - -export class URLServiceChannelClient implements IURLService { - - _serviceBrand: any; - - constructor(private channel: IChannel) { } - - open(url: URI): TPromise { - return this.channel.call('open', url.toJSON()); - } - - registerHandler(handler: IURLHandler): IDisposable { - throw new Error('Not implemented.'); - } -} - -export interface IURLHandlerChannel extends IChannel { - call(command: 'handleURL', arg: any): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class URLHandlerChannel implements IURLHandlerChannel { - - constructor(private handler: IURLHandler) { } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'handleURL': return this.handler.handleURL(URI.revive(arg)); - } - return undefined; - } -} - -export class URLHandlerChannelClient implements IURLHandler { - - constructor(private channel: IChannel) { } - - handleURL(uri: URI): TPromise { - return this.channel.call('handleURL', uri.toJSON()); - } -} \ No newline at end of file diff --git a/src/vs/platform/url/common/urlService.ts b/src/vs/platform/url/common/urlService.ts index f4fbab19d7d..0d71301aac0 100644 --- a/src/vs/platform/url/common/urlService.ts +++ b/src/vs/platform/url/common/urlService.ts @@ -6,9 +6,10 @@ 'use strict'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; +import { first } from 'vs/base/common/async'; declare module Array { function from(set: Set): T[]; @@ -20,16 +21,9 @@ export class URLService implements IURLService { private handlers = new Set(); - async open(uri: URI): TPromise { + open(uri: URI): TPromise { const handlers = Array.from(this.handlers); - - for (const handler of handlers) { - if (await handler.handleURL(uri)) { - return true; - } - } - - return false; + return first(handlers.map(h => () => h.handleURL(uri)), undefined, false); } registerHandler(handler: IURLHandler): IDisposable { @@ -44,7 +38,7 @@ export class RelayURLService extends URLService implements IURLHandler { super(); } - async open(uri: URI): TPromise { + open(uri: URI): TPromise { return this.urlService.open(uri); } diff --git a/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts b/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts deleted file mode 100644 index f8c42720026..00000000000 --- a/src/vs/platform/url/electron-browser/inactiveExtensionUrlHandler.ts +++ /dev/null @@ -1,149 +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 { IURLService, IURLHandler } from 'vs/platform/url/common/url'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { localize } from 'vs/nls'; - -const FIVE_MINUTES = 5 * 60 * 1000; -const THIRTY_SECONDS = 30 * 1000; - -function isExtensionId(value: string): boolean { - return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value); -} - -export const IExtensionUrlHandler = createDecorator('inactiveExtensionUrlHandler'); - -export interface IExtensionUrlHandler { - readonly _serviceBrand: any; - registerExtensionHandler(extensionId: string, handler: IURLHandler): void; - unregisterExtensionHandler(extensionId: string): void; -} - -/** - * This class handles URLs which are directed towards inactive extensions. - * If a URL is directed towards an inactive extension, it buffers it, - * activates the extension and re-opens the URL once the extension registers - * a URL handler. If the extension never registers a URL handler, the urls - * will eventually be garbage collected. - * - * It also makes sure the user confirms opening URLs directed towards extensions. - */ -export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { - - readonly _serviceBrand: any; - - private extensionHandlers = new Map(); - private uriBuffer = new Map(); - private disposable: IDisposable; - - constructor( - @IURLService urlService: IURLService, - @IExtensionService private extensionService: IExtensionService, - @IDialogService private dialogService: IDialogService - ) { - const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS); - - this.disposable = combinedDisposable([ - urlService.registerHandler(this), - toDisposable(() => clearInterval(interval)) - ]); - } - - async handleURL(uri: URI): TPromise { - if (!isExtensionId(uri.authority)) { - return false; - } - - const extensionId = uri.authority; - const wasHandlerAvailable = this.extensionHandlers.has(extensionId); - - const extensions = await this.extensionService.getExtensions(); - const extension = extensions.filter(e => e.id === extensionId)[0]; - - if (!extension) { - return false; - } - - const result = await this.dialogService.confirm({ - message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId), - detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}` - }); - - if (!result.confirmed) { - return true; - } - - const handler = this.extensionHandlers.get(extensionId); - if (handler) { - if (!wasHandlerAvailable) { - // forward it directly - return handler.handleURL(uri); - } - - // let the ExtensionUrlHandler instance handle this - return TPromise.as(false); - } - - // collect URI for eventual extension activation - const timestamp = new Date().getTime(); - let uris = this.uriBuffer.get(extensionId); - - if (!uris) { - uris = []; - this.uriBuffer.set(extensionId, uris); - } - - uris.push({ timestamp, uri }); - - // activate the extension - await this.extensionService.activateByEvent(`onUri:${extensionId}`); - - return true; - } - - registerExtensionHandler(extensionId: string, handler: IURLHandler): void { - this.extensionHandlers.set(extensionId, handler); - - const uris = this.uriBuffer.get(extensionId) || []; - - for (const { uri } of uris) { - handler.handleURL(uri); - } - - this.uriBuffer.delete(extensionId); - } - - unregisterExtensionHandler(extensionId: string): void { - this.extensionHandlers.delete(extensionId); - } - - // forget about all uris buffered more than 5 minutes ago - private garbageCollect(): void { - const now = new Date().getTime(); - const uriBuffer = new Map(); - - this.uriBuffer.forEach((uris, extensionId) => { - uris = uris.filter(({ timestamp }) => now - timestamp < FIVE_MINUTES); - - if (uris.length > 0) { - uriBuffer.set(extensionId, uris); - } - }); - - this.uriBuffer = uriBuffer; - } - - dispose(): void { - this.disposable.dispose(); - this.extensionHandlers.clear(); - this.uriBuffer.clear(); - } -} \ No newline at end of file diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index b3b0fdf8bd1..1e5fac2620f 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -9,7 +9,7 @@ import { mapEvent, fromNodeEventEmitter, filterEvent, once } from 'vs/base/commo import { IURLService } from 'vs/platform/url/common/url'; import product from 'vs/platform/node/product'; import { app } from 'electron'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { ReadyState } from 'vs/platform/windows/common/windows'; diff --git a/src/vs/platform/url/node/urlIpc.ts b/src/vs/platform/url/node/urlIpc.ts new file mode 100644 index 00000000000..7122ab17100 --- /dev/null +++ b/src/vs/platform/url/node/urlIpc.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { URI } from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; + +export interface IURLServiceChannel extends IChannel { + call(command: 'open', url: string): Thenable; + call(command: string, arg?: any): Thenable; +} + +export class URLServiceChannel implements IURLServiceChannel { + + constructor(private service: IURLService) { } + + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'open': return this.service.open(URI.revive(arg)); + } + return undefined; + } +} + +export class URLServiceChannelClient implements IURLService { + + _serviceBrand: any; + + constructor(private channel: IChannel) { } + + open(url: URI): TPromise { + return TPromise.wrap(this.channel.call('open', url.toJSON())); + } + + registerHandler(handler: IURLHandler): IDisposable { + throw new Error('Not implemented.'); + } +} + +export interface IURLHandlerChannel extends IChannel { + call(command: 'handleURL', arg: any): Thenable; + call(command: string, arg?: any): Thenable; +} + +export class URLHandlerChannel implements IURLHandlerChannel { + + constructor(private handler: IURLHandler) { } + + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'handleURL': return this.handler.handleURL(URI.revive(arg)); + } + return undefined; + } +} + +export class URLHandlerChannelClient implements IURLHandler { + + constructor(private channel: IChannel) { } + + handleURL(uri: URI): TPromise { + return TPromise.wrap(this.channel.call('handleURL', uri.toJSON())); + } +} \ No newline at end of file diff --git a/src/vs/platform/widget/browser/contextScopedHistoryWidget.ts b/src/vs/platform/widget/browser/contextScopedHistoryWidget.ts new file mode 100644 index 00000000000..013485d5e42 --- /dev/null +++ b/src/vs/platform/widget/browser/contextScopedHistoryWidget.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IContextKeyService, ContextKeyDefinedExpr, ContextKeyExpr, ContextKeyAndExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; +import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { IContextScopedWidget, getContextScopedWidget, createWidgetScopedContextKeyService, bindContextScopedWidget } from 'vs/platform/widget/common/contextScopedWidget'; +import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; + +export const HistoryNavigationWidgetContext = 'historyNavigationWidget'; +export const HistoryNavigationEnablementContext = 'historyNavigationEnabled'; + +export interface IContextScopedHistoryNavigationWidget extends IContextScopedWidget { + + historyNavigator: IHistoryNavigationWidget; + +} + +export function createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedHistoryNavigationWidget): { scopedContextKeyService: IContextKeyService, historyNavigationEnablement: IContextKey } { + const scopedContextKeyService = createWidgetScopedContextKeyService(contextKeyService, widget); + bindContextScopedWidget(scopedContextKeyService, widget, HistoryNavigationWidgetContext); + const historyNavigationEnablement = new RawContextKey(HistoryNavigationEnablementContext, true).bindTo(scopedContextKeyService); + return { scopedContextKeyService, historyNavigationEnablement }; +} + +export class ContextScopedHistoryInputBox extends HistoryInputBox { + + constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IHistoryInputOptions, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(container, contextViewProvider, options); + this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.element, historyNavigator: this }).scopedContextKeyService); + } + +} + +export class ContextScopedFindInput extends FindInput { + + constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IFindInputOptions, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(container, contextViewProvider, options); + this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService); + } + +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'history.showPrevious', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)), + primary: KeyCode.UpArrow, + secondary: [KeyMod.Alt | KeyCode.UpArrow], + handler: (accessor, arg2) => { + const historyInputBox: IHistoryNavigationWidget = getContextScopedWidget(accessor.get(IContextKeyService), HistoryNavigationWidgetContext).historyNavigator; + historyInputBox.showPreviousValue(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'history.showNext', + weight: KeybindingWeight.WorkbenchContrib, + when: new ContextKeyAndExpr([new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)]), + primary: KeyCode.DownArrow, + secondary: [KeyMod.Alt | KeyCode.DownArrow], + handler: (accessor, arg2) => { + const historyInputBox: IHistoryNavigationWidget = getContextScopedWidget(accessor.get(IContextKeyService), HistoryNavigationWidgetContext).historyNavigator; + historyInputBox.showNextValue(); + } +}); diff --git a/src/vs/platform/widget/browser/input.ts b/src/vs/platform/widget/browser/input.ts deleted file mode 100644 index 51cdcbb5b72..00000000000 --- a/src/vs/platform/widget/browser/input.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; -import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; -import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { createWidgetScopedContextKeyService, IWidget } from 'vs/platform/widget/browser/widget'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; -import { localize } from 'vs/nls'; - -export const HistoryInputBoxContext = 'historyInputBox'; - -export class ContextScopedHistoryInputBox extends HistoryInputBox implements IWidget { - - constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IHistoryInputOptions, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(container, contextViewProvider, options); - this._register(createWidgetScopedContextKeyService(contextKeyService, this, HistoryInputBoxContext)); - } -} - -export class ContextScopedFindInput extends FindInput { - - constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IFindInputOptions, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(container, contextViewProvider, options); - this._register(createWidgetScopedContextKeyService(contextKeyService, this.inputBox, HistoryInputBoxContext)); - } - -} - -export function showDeprecatedWarning(notificationService: INotificationService, keybindingService: IKeybindingService, storageService: IStorageService): void { - const previousCommand = 'input.action.historyPrevious'; - const nextCommand = 'input.action.historyNext'; - let previousKeybinding: ResolvedKeybindingItem, nextKeybinding: ResolvedKeybindingItem; - for (const keybinding of keybindingService.getKeybindings()) { - if (keybinding.command === previousCommand) { - if (!keybinding.isDefault) { - return; - } - previousKeybinding = keybinding; - } - if (keybinding.command === nextCommand) { - if (!keybinding.isDefault) { - return; - } - nextKeybinding = keybinding; - } - } - const key = 'donotshow.historyNavigation.warning'; - if (!storageService.getBoolean(key, StorageScope.GLOBAL, false)) { - const message = localize('showDeprecatedWarningMessage', "History navigation commands you are using are deprecated. Instead use following new commands: {0} and {1}", `${previousCommand} (${previousKeybinding.resolvedKeybinding.getLabel()})`, `${nextCommand} (${nextKeybinding.resolvedKeybinding.getLabel()})`); - notificationService.prompt(Severity.Warning, message, [ - { - label: localize('more information', "More Information..."), - run: () => null - }, - { - label: localize('Do not show again', "Don't show again"), - isSecondary: true, - run: () => storageService.store(key, true, StorageScope.GLOBAL) - } - ]); - } -} \ No newline at end of file diff --git a/src/vs/platform/widget/browser/widget.contribution.ts b/src/vs/platform/widget/browser/widget.contribution.ts deleted file mode 100644 index 894ec6870ce..00000000000 --- a/src/vs/platform/widget/browser/widget.contribution.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { ContextKeyDefinedExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { HistoryInputBoxContext } from 'vs/platform/widget/browser/input'; -import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'input.action.historyPrevious', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: new ContextKeyDefinedExpr(HistoryInputBoxContext), - primary: KeyCode.UpArrow, - handler: (accessor, arg2) => { - const historyInputBox: HistoryInputBox = accessor.get(IContextKeyService).getContext(document.activeElement).getValue(HistoryInputBoxContext); - historyInputBox.showPreviousValue(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'input.action.historyNext', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: new ContextKeyDefinedExpr(HistoryInputBoxContext), - primary: KeyCode.DownArrow, - handler: (accessor, arg2) => { - const historyInputBox: HistoryInputBox = accessor.get(IContextKeyService).getContext(document.activeElement).getValue(HistoryInputBoxContext); - historyInputBox.showNextValue(); - } -}); diff --git a/src/vs/platform/widget/browser/widget.ts b/src/vs/platform/widget/browser/widget.ts deleted file mode 100644 index a69bc135694..00000000000 --- a/src/vs/platform/widget/browser/widget.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export function createWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IWidget, contextKey: string): IContextKeyService { - const result = contextKeyService.createScoped(widget.element); - const widgetContext = new RawContextKey(contextKey, widget); - widgetContext.bindTo(result); - return result; -} - -export interface IWidget { - - readonly element: HTMLElement; - -} \ No newline at end of file diff --git a/src/vs/platform/widget/common/contextScopedWidget.ts b/src/vs/platform/widget/common/contextScopedWidget.ts new file mode 100644 index 00000000000..4a552485026 --- /dev/null +++ b/src/vs/platform/widget/common/contextScopedWidget.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IContextKeyService, RawContextKey, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; + +export function bindContextScopedWidget(contextKeyService: IContextKeyService, widget: IContextScopedWidget, contextKey: string): void { + new RawContextKey(contextKey, widget).bindTo(contextKeyService); +} + +export function createWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedWidget): IContextKeyService { + return contextKeyService.createScoped(widget.target); +} + +export function getContextScopedWidget(contextKeyService: IContextKeyService, contextKey: string): T { + return contextKeyService.getContext(document.activeElement).getValue(contextKey); +} + +export interface IContextScopedWidget { + + readonly target: IContextKeyServiceTarget; + +} \ No newline at end of file diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index f548718cd75..d82d7c8227a 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -11,12 +11,13 @@ import { Event, latch, anyEvent } from 'vs/base/common/event'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; -import { ICommandAction } from 'vs/platform/actions/common/actions'; -import { PerformanceEntry } from 'vs/base/common/performance'; +import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { ExportData } from 'vs/base/common/performance'; import { LogLevel } from 'vs/platform/log/common/log'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; export const IWindowsService = createDecorator('windowsService'); @@ -103,6 +104,9 @@ export interface IWindowsService { onWindowOpen: Event; onWindowFocus: Event; onWindowBlur: Event; + onWindowMaximize: Event; + onWindowUnmaximize: Event; + onRecentlyOpenedChange: Event; // Dialogs pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise; @@ -117,12 +121,13 @@ export interface IWindowsService { openDevTools(windowId: number, options?: IDevToolsOptions): TPromise; toggleDevTools(windowId: number): TPromise; closeWorkspace(windowId: number): TPromise; + enterWorkspace(windowId: number, path: string): TPromise; createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(windowId: number, path: string): TPromise; toggleFullScreen(windowId: number): TPromise; setRepresentedFilename(windowId: number, fileName: string): TPromise; - addRecentlyOpened(files: string[]): TPromise; - removeFromRecentlyOpened(paths: string[]): TPromise; + addRecentlyOpened(files: URI[]): TPromise; + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[]): TPromise; clearRecentlyOpened(): TPromise; getRecentlyOpened(windowId: number): TPromise; focusWindow(windowId: number): TPromise; @@ -131,12 +136,14 @@ export interface IWindowsService { isMaximized(windowId: number): TPromise; maximizeWindow(windowId: number): TPromise; unmaximizeWindow(windowId: number): TPromise; + minimizeWindow(windowId: number): TPromise; onWindowTitleDoubleClick(windowId: number): TPromise; setDocumentEdited(windowId: number, flag: boolean): TPromise; quit(): TPromise; relaunch(options: { addArgs?: string[], removeArgs?: string[] }): TPromise; // macOS Native Tabs + newWindowTab(): TPromise; showPreviousWindowTab(): TPromise; showNextWindowTab(): TPromise; moveWindowTabToNewWindow(): TPromise; @@ -144,20 +151,21 @@ export interface IWindowsService { toggleWindowTabsBar(): TPromise; // macOS TouchBar - updateTouchBar(windowId: number, items: ICommandAction[][]): TPromise; + updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): TPromise; // Shared process whenSharedProcessReady(): TPromise; toggleSharedProcess(): TPromise; // Global methods - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise; + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise; openNewWindow(): TPromise; showWindow(windowId: number): TPromise; - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; getWindowCount(): TPromise; log(severity: string, ...messages: string[]): TPromise; showItemInFolder(path: string): TPromise; + getActiveWindowId(): TPromise; // This needs to be handled from browser process to prevent // foreground ordering issues on Windows @@ -181,6 +189,7 @@ export interface IWindowService { _serviceBrand: any; onDidChangeFocus: Event; + onDidChangeMaximize: Event; getConfiguration(): IWindowConfiguration; getCurrentWindowId(): number; @@ -192,7 +201,8 @@ export interface IWindowService { openDevTools(options?: IDevToolsOptions): TPromise; toggleDevTools(): TPromise; closeWorkspace(): TPromise; - updateTouchBar(items: ICommandAction[][]): TPromise; + updateTouchBar(items: ISerializableCommandAction[][]): TPromise; + enterWorkspace(path: string): TPromise; createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(path: string): TPromise; toggleFullScreen(): TPromise; @@ -200,9 +210,13 @@ export interface IWindowService { getRecentlyOpened(): TPromise; focusWindow(): TPromise; closeWindow(): TPromise; - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise; + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise; isFocused(): TPromise; setDocumentEdited(flag: boolean): TPromise; + isMaximized(): TPromise; + maximizeWindow(): TPromise; + unmaximizeWindow(): TPromise; + minimizeWindow(): TPromise; onWindowTitleDoubleClick(): TPromise; show(): TPromise; showMessageBox(options: MessageBoxOptions): TPromise; @@ -234,7 +248,7 @@ export interface IWindowSettings { clickThroughInactive: boolean; } -export enum OpenContext { +export const enum OpenContext { // opening when running from the command line CLI, @@ -255,7 +269,7 @@ export enum OpenContext { API } -export enum ReadyState { +export const enum ReadyState { /** * This window has not loaded any HTML yet @@ -278,10 +292,25 @@ export enum ReadyState { READY } -export interface IPath { +export interface IPath extends IPathData { // the file path to open within a Code instance - filePath?: string; + fileUri?: URI; +} + +export interface IPathsToWaitFor extends IPathsToWaitForData { + paths: IPath[]; +} + +export interface IPathsToWaitForData { + paths: IPathData[]; + waitMarkerFilePath: string; +} + +export interface IPathData { + + // the file path to open within a Code instance + fileUri?: UriComponents; // the line number in the file path to open lineNumber?: number; @@ -290,28 +319,25 @@ export interface IPath { columnNumber?: number; } -export interface IPathsToWaitFor { - paths: IPath[]; - waitMarkerFilePath: string; -} - export interface IOpenFileRequest { - filesToOpen?: IPath[]; - filesToCreate?: IPath[]; - filesToDiff?: IPath[]; - filesToWait?: IPathsToWaitFor; + filesToOpen?: IPathData[]; + filesToCreate?: IPathData[]; + filesToDiff?: IPathData[]; + filesToWait?: IPathsToWaitForData; termProgram?: string; } export interface IAddFoldersRequest { - foldersToAdd: IPath[]; + foldersToAdd: UriComponents[]; } -export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { +export interface IWindowConfiguration extends ParsedArgs { machineId: string; windowId: number; logLevel: LogLevel; + mainPid: number; + appRoot: string; execPath: string; isInitialStartup?: boolean; @@ -322,19 +348,25 @@ export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { backupPath?: string; workspace?: IWorkspaceIdentifier; - folderPath?: string; + folderUri?: ISingleFolderWorkspaceIdentifier; zoomLevel?: number; fullscreen?: boolean; + maximized?: boolean; highContrast?: boolean; - baseTheme?: string; - backgroundColor?: string; + frameless?: boolean; accessibilitySupport?: boolean; - perfEntries: PerformanceEntry[]; perfStartTime?: number; perfAppReady?: number; perfWindowLoadTime?: number; + perfEntries: ExportData; + + filesToOpen?: IPath[]; + filesToCreate?: IPath[]; + filesToDiff?: IPath[]; + filesToWait?: IPathsToWaitFor; + termProgram?: string; } export interface IRunActionInWindowRequest { @@ -345,22 +377,34 @@ export interface IRunActionInWindowRequest { export class ActiveWindowManager implements IDisposable { private disposables: IDisposable[] = []; - private _activeWindowId: number; + private firstActiveWindowIdPromise: TPromise | null; + private _activeWindowId: number | undefined; constructor(@IWindowsService windowsService: IWindowsService) { const onActiveWindowChange = latch(anyEvent(windowsService.onWindowOpen, windowsService.onWindowFocus)); onActiveWindowChange(this.setActiveWindow, this, this.disposables); + + this.firstActiveWindowIdPromise = windowsService.getActiveWindowId() + .then(id => (typeof this._activeWindowId === 'undefined') && this.setActiveWindow(id)); } private setActiveWindow(windowId: number) { + if (this.firstActiveWindowIdPromise) { + this.firstActiveWindowIdPromise = null; + } + this._activeWindowId = windowId; } - get activeClientId(): string { - return `window:${this._activeWindowId}`; + getActiveClientId(): TPromise { + if (this.firstActiveWindowIdPromise) { + return this.firstActiveWindowIdPromise; + } + + return TPromise.as(`window:${this._activeWindowId}`); } dispose() { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts deleted file mode 100644 index 86b8cf27dfd..00000000000 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ /dev/null @@ -1,355 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { Event, buffer } from 'vs/base/common/event'; -import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; -import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions } from 'vs/platform/windows/common/windows'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; -import { IRecentlyOpened } from 'vs/platform/history/common/history'; -import { ICommandAction } from 'vs/platform/actions/common/actions'; -import URI from 'vs/base/common/uri'; -import { ParsedArgs } from 'vs/platform/environment/common/environment'; - -export interface IWindowsChannel extends IChannel { - call(command: 'event:onWindowOpen'): TPromise; - call(command: 'event:onWindowFocus'): TPromise; - call(command: 'event:onWindowBlur'): TPromise; - call(command: 'pickFileFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; - call(command: 'pickFileAndOpen', arg: INativeOpenDialogOptions): TPromise; - call(command: 'pickFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; - call(command: 'pickWorkspaceAndOpen', arg: INativeOpenDialogOptions): TPromise; - call(command: 'showMessageBox', arg: [number, MessageBoxOptions]): TPromise; - call(command: 'showSaveDialog', arg: [number, SaveDialogOptions]): TPromise; - call(command: 'showOpenDialog', arg: [number, OpenDialogOptions]): TPromise; - call(command: 'reloadWindow', arg: [number, ParsedArgs]): TPromise; - call(command: 'toggleDevTools', arg: number): TPromise; - call(command: 'closeWorkspace', arg: number): TPromise; - call(command: 'createAndEnterWorkspace', arg: [number, IWorkspaceFolderCreationData[], string]): TPromise; - call(command: 'saveAndEnterWorkspace', arg: [number, string]): TPromise; - call(command: 'toggleFullScreen', arg: number): TPromise; - call(command: 'setRepresentedFilename', arg: [number, string]): TPromise; - call(command: 'addRecentlyOpened', arg: string[]): TPromise; - call(command: 'removeFromRecentlyOpened', arg: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): TPromise; - call(command: 'clearRecentlyOpened'): TPromise; - call(command: 'getRecentlyOpened', arg: number): TPromise; - call(command: 'showPreviousWindowTab', arg: number): TPromise; - call(command: 'showNextWindowTab', arg: number): TPromise; - call(command: 'moveWindowTabToNewWindow', arg: number): TPromise; - call(command: 'mergeAllWindowTabs', arg: number): TPromise; - call(command: 'toggleWindowTabsBar', arg: number): TPromise; - call(command: 'updateTouchBar', arg: [number, ICommandAction[][]]): TPromise; - call(command: 'focusWindow', arg: number): TPromise; - call(command: 'closeWindow', arg: number): TPromise; - call(command: 'isFocused', arg: number): TPromise; - call(command: 'isMaximized', arg: number): TPromise; - call(command: 'maximizeWindow', arg: number): TPromise; - call(command: 'unmaximizeWindow', arg: number): TPromise; - call(command: 'onWindowTitleDoubleClick', arg: number): TPromise; - call(command: 'setDocumentEdited', arg: [number, boolean]): TPromise; - call(command: 'quit'): TPromise; - call(command: 'openWindow', arg: [number, string[], { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }]): TPromise; - call(command: 'openNewWindow'): TPromise; - call(command: 'showWindow', arg: number): TPromise; - call(command: 'getWindows'): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]>; - call(command: 'getWindowCount'): TPromise; - call(command: 'relaunch', arg: { addArgs?: string[], removeArgs?: string[] }): TPromise; - call(command: 'whenSharedProcessReady'): TPromise; - call(command: 'toggleSharedProcess'): TPromise; - call(command: 'log', arg: [string, string[]]): TPromise; - call(command: 'showItemInFolder', arg: string): TPromise; - call(command: 'openExternal', arg: string): TPromise; - call(command: 'startCrashReporter', arg: CrashReporterStartOptions): TPromise; - call(command: 'openAboutDialog'): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class WindowsChannel implements IWindowsChannel { - - private onWindowOpen: Event; - private onWindowFocus: Event; - private onWindowBlur: Event; - - constructor(private service: IWindowsService) { - this.onWindowOpen = buffer(service.onWindowOpen, true); - this.onWindowFocus = buffer(service.onWindowFocus, true); - this.onWindowBlur = buffer(service.onWindowBlur, true); - } - - call(command: string, arg?: any): TPromise { - switch (command) { - case 'event:onWindowOpen': return eventToCall(this.onWindowOpen); - case 'event:onWindowFocus': return eventToCall(this.onWindowFocus); - case 'event:onWindowBlur': return eventToCall(this.onWindowBlur); - case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg); - case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg); - case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg); - case 'pickWorkspaceAndOpen': return this.service.pickWorkspaceAndOpen(arg); - case 'showMessageBox': return this.service.showMessageBox(arg[0], arg[1]); - case 'showSaveDialog': return this.service.showSaveDialog(arg[0], arg[1]); - case 'showOpenDialog': return this.service.showOpenDialog(arg[0], arg[1]); - case 'reloadWindow': return this.service.reloadWindow(arg[0], arg[1]); - 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 'createAndEnterWorkspace': { - const rawFolders: IWorkspaceFolderCreationData[] = arg[1]; - let folders: IWorkspaceFolderCreationData[]; - 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 'toggleFullScreen': return this.service.toggleFullScreen(arg); - case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]); - case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg); - case 'removeFromRecentlyOpened': return this.service.removeFromRecentlyOpened(arg); - case 'clearRecentlyOpened': return this.service.clearRecentlyOpened(); - case 'showPreviousWindowTab': return this.service.showPreviousWindowTab(); - case 'showNextWindowTab': return this.service.showNextWindowTab(); - case 'moveWindowTabToNewWindow': return this.service.moveWindowTabToNewWindow(); - case 'mergeAllWindowTabs': return this.service.mergeAllWindowTabs(); - case 'toggleWindowTabsBar': return this.service.toggleWindowTabsBar(); - case 'updateTouchBar': return this.service.updateTouchBar(arg[0], arg[1]); - case 'getRecentlyOpened': return this.service.getRecentlyOpened(arg); - case 'focusWindow': return this.service.focusWindow(arg); - case 'closeWindow': return this.service.closeWindow(arg); - case 'isFocused': return this.service.isFocused(arg); - case 'isMaximized': return this.service.isMaximized(arg); - case 'maximizeWindow': return this.service.maximizeWindow(arg); - case 'unmaximizeWindow': return this.service.unmaximizeWindow(arg); - case 'onWindowTitleDoubleClick': return this.service.onWindowTitleDoubleClick(arg); - case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); - case 'openWindow': return this.service.openWindow(arg[0], arg[1], arg[2]); - case 'openNewWindow': return this.service.openNewWindow(); - case 'showWindow': return this.service.showWindow(arg); - case 'getWindows': return this.service.getWindows(); - case 'getWindowCount': return this.service.getWindowCount(); - case 'relaunch': return this.service.relaunch(arg[0]); - case 'whenSharedProcessReady': return this.service.whenSharedProcessReady(); - case 'toggleSharedProcess': return this.service.toggleSharedProcess(); - case 'quit': return this.service.quit(); - case 'log': return this.service.log(arg[0], arg[1]); - case 'showItemInFolder': return this.service.showItemInFolder(arg); - case 'openExternal': return this.service.openExternal(arg); - case 'startCrashReporter': return this.service.startCrashReporter(arg); - case 'openAboutDialog': return this.service.openAboutDialog(); - } - return undefined; - } -} - -export class WindowsChannelClient implements IWindowsService { - - _serviceBrand: any; - - constructor(private channel: IWindowsChannel) { } - - private _onWindowOpen: Event = eventFromCall(this.channel, 'event:onWindowOpen'); - get onWindowOpen(): Event { return this._onWindowOpen; } - - private _onWindowFocus: Event = eventFromCall(this.channel, 'event:onWindowFocus'); - get onWindowFocus(): Event { return this._onWindowFocus; } - - private _onWindowBlur: Event = eventFromCall(this.channel, 'event:onWindowBlur'); - get onWindowBlur(): Event { return this._onWindowBlur; } - - pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { - return this.channel.call('pickFileFolderAndOpen', options); - } - - pickFileAndOpen(options: INativeOpenDialogOptions): TPromise { - return this.channel.call('pickFileAndOpen', options); - } - - pickFolderAndOpen(options: INativeOpenDialogOptions): TPromise { - return this.channel.call('pickFolderAndOpen', options); - } - - pickWorkspaceAndOpen(options: INativeOpenDialogOptions): TPromise { - return this.channel.call('pickWorkspaceAndOpen', options); - } - - showMessageBox(windowId: number, options: MessageBoxOptions): TPromise { - return this.channel.call('showMessageBox', [windowId, options]); - } - - showSaveDialog(windowId: number, options: SaveDialogOptions): TPromise { - return this.channel.call('showSaveDialog', [windowId, options]); - } - - showOpenDialog(windowId: number, options: OpenDialogOptions): TPromise { - return this.channel.call('showOpenDialog', [windowId, options]); - } - - reloadWindow(windowId: number, args?: ParsedArgs): TPromise { - return this.channel.call('reloadWindow', [windowId, args]); - } - - openDevTools(windowId: number, options?: IDevToolsOptions): TPromise { - return this.channel.call('openDevTools', [windowId, options]); - } - - toggleDevTools(windowId: number): TPromise { - return this.channel.call('toggleDevTools', windowId); - } - - closeWorkspace(windowId: number): TPromise { - return this.channel.call('closeWorkspace', windowId); - } - - createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { - return this.channel.call('createAndEnterWorkspace', [windowId, folders, path]); - } - - saveAndEnterWorkspace(windowId: number, path: string): TPromise { - return this.channel.call('saveAndEnterWorkspace', [windowId, path]); - } - - toggleFullScreen(windowId: number): TPromise { - return this.channel.call('toggleFullScreen', windowId); - } - - setRepresentedFilename(windowId: number, fileName: string): TPromise { - return this.channel.call('setRepresentedFilename', [windowId, fileName]); - } - - addRecentlyOpened(files: string[]): TPromise { - return this.channel.call('addRecentlyOpened', files); - } - - removeFromRecentlyOpened(paths: string[]): TPromise { - return this.channel.call('removeFromRecentlyOpened', paths); - } - - clearRecentlyOpened(): TPromise { - return this.channel.call('clearRecentlyOpened'); - } - - getRecentlyOpened(windowId: number): TPromise { - return this.channel.call('getRecentlyOpened', windowId); - } - - showPreviousWindowTab(): TPromise { - return this.channel.call('showPreviousWindowTab'); - } - - showNextWindowTab(): TPromise { - return this.channel.call('showNextWindowTab'); - } - - moveWindowTabToNewWindow(): TPromise { - return this.channel.call('moveWindowTabToNewWindow'); - } - - mergeAllWindowTabs(): TPromise { - return this.channel.call('mergeAllWindowTabs'); - } - - toggleWindowTabsBar(): TPromise { - return this.channel.call('toggleWindowTabsBar'); - } - - focusWindow(windowId: number): TPromise { - return this.channel.call('focusWindow', windowId); - } - - closeWindow(windowId: number): TPromise { - return this.channel.call('closeWindow', windowId); - } - - isFocused(windowId: number): TPromise { - return this.channel.call('isFocused', windowId); - } - - isMaximized(windowId: number): TPromise { - return this.channel.call('isMaximized', windowId); - } - - maximizeWindow(windowId: number): TPromise { - return this.channel.call('maximizeWindow', windowId); - } - - unmaximizeWindow(windowId: number): TPromise { - return this.channel.call('unmaximizeWindow', windowId); - } - - onWindowTitleDoubleClick(windowId: number): TPromise { - return this.channel.call('onWindowTitleDoubleClick', windowId); - } - - setDocumentEdited(windowId: number, flag: boolean): TPromise { - return this.channel.call('setDocumentEdited', [windowId, flag]); - } - - quit(): TPromise { - return this.channel.call('quit'); - } - - relaunch(options: { addArgs?: string[], removeArgs?: string[] }): TPromise { - return this.channel.call('relaunch', [options]); - } - - whenSharedProcessReady(): TPromise { - return this.channel.call('whenSharedProcessReady'); - } - - toggleSharedProcess(): TPromise { - return this.channel.call('toggleSharedProcess'); - } - - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { - return this.channel.call('openWindow', [windowId, paths, options]); - } - - openNewWindow(): TPromise { - return this.channel.call('openNewWindow'); - } - - showWindow(windowId: number): TPromise { - return this.channel.call('showWindow', windowId); - } - - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { - return this.channel.call('getWindows'); - } - - getWindowCount(): TPromise { - return this.channel.call('getWindowCount'); - } - - log(severity: string, ...messages: string[]): TPromise { - return this.channel.call('log', [severity, messages]); - } - - showItemInFolder(path: string): TPromise { - return this.channel.call('showItemInFolder', path); - } - - openExternal(url: string): TPromise { - return this.channel.call('openExternal', url); - } - - startCrashReporter(config: CrashReporterStartOptions): TPromise { - return this.channel.call('startCrashReporter', config); - } - - updateTouchBar(windowId: number, items: ICommandAction[][]): TPromise { - return this.channel.call('updateTouchBar', [windowId, items]); - } - - openAboutDialog(): TPromise { - return this.channel.call('openAboutDialog'); - } -} diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 785106fbd91..ab83d870ea5 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -9,13 +9,15 @@ import { Event, filterEvent, mapEvent, anyEvent } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, IDevToolsOptions } from 'vs/platform/windows/common/windows'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; -import { ICommandAction } from 'vs/platform/actions/common/actions'; +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'; export class WindowService implements IWindowService { readonly onDidChangeFocus: Event; + readonly onDidChangeMaximize: Event; _serviceBrand: any; @@ -26,7 +28,10 @@ export class WindowService implements IWindowService { ) { const onThisWindowFocus = mapEvent(filterEvent(windowsService.onWindowFocus, id => id === windowId), _ => true); const onThisWindowBlur = mapEvent(filterEvent(windowsService.onWindowBlur, id => id === windowId), _ => false); + const onThisWindowMaximize = mapEvent(filterEvent(windowsService.onWindowMaximize, id => id === windowId), _ => true); + const onThisWindowUnmaximize = mapEvent(filterEvent(windowsService.onWindowUnmaximize, id => id === windowId), _ => false); this.onDidChangeFocus = anyEvent(onThisWindowFocus, onThisWindowBlur); + this.onDidChangeMaximize = anyEvent(onThisWindowMaximize, onThisWindowUnmaximize); } getCurrentWindowId(): number { @@ -77,6 +82,10 @@ export class WindowService implements IWindowService { return this.windowsService.closeWorkspace(this.windowId); } + enterWorkspace(path: string): TPromise { + return this.windowsService.enterWorkspace(this.windowId, path); + } + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.windowsService.createAndEnterWorkspace(this.windowId, folders, path); } @@ -85,7 +94,7 @@ export class WindowService implements IWindowService { return this.windowsService.saveAndEnterWorkspace(this.windowId, path); } - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise { + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { return this.windowsService.openWindow(this.windowId, paths, options); } @@ -113,6 +122,22 @@ export class WindowService implements IWindowService { return this.windowsService.isFocused(this.windowId); } + isMaximized(): TPromise { + return this.windowsService.isMaximized(this.windowId); + } + + maximizeWindow(): TPromise { + return this.windowsService.maximizeWindow(this.windowId); + } + + unmaximizeWindow(): TPromise { + return this.windowsService.unmaximizeWindow(this.windowId); + } + + minimizeWindow(): TPromise { + return this.windowsService.minimizeWindow(this.windowId); + } + onWindowTitleDoubleClick(): TPromise { return this.windowsService.onWindowTitleDoubleClick(this.windowId); } @@ -137,7 +162,7 @@ export class WindowService implements IWindowService { return this.windowsService.showOpenDialog(this.windowId, options); } - updateTouchBar(items: ICommandAction[][]): TPromise { + updateTouchBar(items: ISerializableCommandAction[][]): TPromise { return this.windowsService.updateTouchBar(this.windowId, items); } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index ce2eba8895b..66bdfc05a75 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -12,7 +12,8 @@ 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 { ICommandAction } from 'vs/platform/actions/common/actions'; +import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; +import { URI } from 'vs/base/common/uri'; export interface IWindowState { width?: number; @@ -23,7 +24,7 @@ export interface IWindowState { display?: number; } -export enum WindowMode { +export const enum WindowMode { Maximized, Normal, Minimized, // not used anymore, but also cannot remove due to existing stored UI state (needs migration) @@ -35,7 +36,7 @@ export interface ICodeWindow { win: Electron.BrowserWindow; config: IWindowConfiguration; - openedFolderPath: string; + openedFolderUri: URI; openedWorkspace: IWorkspaceIdentifier; backupPath: string; @@ -47,6 +48,8 @@ export interface ICodeWindow { readyState: ReadyState; ready(): TPromise; + addTabbedWindow(window: ICodeWindow): void; + load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void; reload(configuration?: IWindowConfiguration, cli?: ParsedArgs): void; @@ -64,7 +67,7 @@ export interface ICodeWindow { getRepresentedFilename(): string; onWindowTitleDoubleClick(): void; - updateTouchBar(items: ICommandAction[][]): void; + updateTouchBar(items: ISerializableCommandAction[][]): void; setReady(): void; serializeWindowState(): IWindowState; @@ -92,6 +95,7 @@ export interface IWindowsMainService { // methods ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; + enterWorkspace(win: ICodeWindow, path: string): TPromise; createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise; saveAndEnterWorkspace(win: ICodeWindow, path: string): TPromise; closeWorkspace(win: ICodeWindow): void; @@ -108,6 +112,7 @@ export interface IWindowsMainService { getLastActiveWindow(): ICodeWindow; waitForWindowCloseOrLoad(windowId: number): TPromise; openNewWindow(context: OpenContext): ICodeWindow[]; + openNewTabbedWindow(context: OpenContext): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void; getFocusedWindow(): ICodeWindow; @@ -122,9 +127,10 @@ export interface IOpenConfiguration { contextWindowId?: number; cli: ParsedArgs; userEnv?: IProcessEnvironment; - pathsToOpen?: string[]; + urisToOpen?: URI[]; preferNewWindow?: boolean; forceNewWindow?: boolean; + forceNewTabbedWindow?: boolean; forceReuseWindow?: boolean; forceEmpty?: boolean; diffMode?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index ebc2d68f0d6..0869cf26629 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -9,21 +9,21 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/node/product'; import { IWindowsService, OpenContext, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IDevToolsOptions } from 'vs/platform/windows/common/windows'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { shell, crashReporter, app, Menu, clipboard } from 'electron'; -import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent } from 'vs/base/common/event'; +import { Event, fromNodeEventEmitter, mapEvent, filterEvent, anyEvent, latch } from 'vs/base/common/event'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IWindowsMainService, ISharedProcess } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; -import { ICommandAction } from 'vs/platform/actions/common/actions'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, 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'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; export class WindowsService implements IWindowsService, IURLHandler, IDisposable { @@ -32,13 +32,18 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable private disposables: IDisposable[] = []; + private _activeWindowId: number | undefined; + readonly onWindowOpen: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-created', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onWindowBlur: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-blur', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onWindowMaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-maximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onWindowUnmaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-unmaximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowFocus: Event = anyEvent( mapEvent(filterEvent(mapEvent(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), w => !!w), w => w.id), filterEvent(fromNodeEventEmitter(app, 'browser-window-focus', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)) ); - readonly onWindowBlur: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-blur', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onRecentlyOpenedChange: Event = this.historyService.onRecentlyOpenedChange; constructor( private sharedProcess: ISharedProcess, @@ -50,6 +55,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable @ILogService private logService: ILogService ) { urlService.registerHandler(this); + + // remember last active window id + latch(anyEvent(this.onWindowOpen, this.onWindowFocus)) + (id => this._activeWindowId = id, null, this.disposables); } pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { @@ -129,7 +138,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable if (codeWindow) { const contents = codeWindow.win.webContents; - if (codeWindow.hasHiddenTitleBarStyle() && !codeWindow.win.isFullScreen() && !contents.isDevToolsOpened()) { + if (isMacintosh && codeWindow.hasHiddenTitleBarStyle() && !codeWindow.win.isFullScreen() && !contents.isDevToolsOpened()) { contents.openDevTools({ mode: 'undocked' }); // due to https://github.com/electron/electron/issues/3647 } else { contents.toggleDevTools(); @@ -139,7 +148,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - updateTouchBar(windowId: number, items: ICommandAction[][]): TPromise { + updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): TPromise { this.logService.trace('windowsService#updateTouchBar', windowId); const codeWindow = this.windowsMainService.getWindowById(windowId); @@ -161,6 +170,17 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + enterWorkspace(windowId: number, path: string): TPromise { + this.logService.trace('windowsService#enterWorkspace', windowId); + const codeWindow = this.windowsMainService.getWindowById(windowId); + + if (codeWindow) { + return this.windowsMainService.enterWorkspace(codeWindow, path); + } + + return TPromise.as(null); + } + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { this.logService.trace('windowsService#createAndEnterWorkspace', windowId); const codeWindow = this.windowsMainService.getWindowById(windowId); @@ -205,14 +225,14 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - addRecentlyOpened(files: string[]): TPromise { + addRecentlyOpened(files: URI[]): TPromise { this.logService.trace('windowsService#addRecentlyOpened'); this.historyService.addRecentlyOpened(void 0, files); return TPromise.as(null); } - removeFromRecentlyOpened(paths: string[]): TPromise { + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[]): TPromise { this.logService.trace('windowsService#removeFromRecentlyOpened'); this.historyService.removeFromRecentlyOpened(paths); @@ -231,12 +251,20 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { - return TPromise.as(this.historyService.getRecentlyOpened(codeWindow.config.workspace || codeWindow.config.folderPath, codeWindow.config.filesToOpen)); + return TPromise.as(this.historyService.getRecentlyOpened(codeWindow.config.workspace || codeWindow.config.folderUri, codeWindow.config.filesToOpen)); } return TPromise.as(this.historyService.getRecentlyOpened()); } + newWindowTab(): TPromise { + this.logService.trace('windowsService#newWindowTab'); + + this.windowsMainService.openNewTabbedWindow(OpenContext.API); + + return TPromise.as(void 0); + } + showPreviousWindowTab(): TPromise { this.logService.trace('windowsService#showPreviousWindowTab'); Menu.sendActionToFirstResponder('selectPreviousTab:'); @@ -338,6 +366,17 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + minimizeWindow(windowId: number): TPromise { + this.logService.trace('windowsService#minimizeWindow', windowId); + const codeWindow = this.windowsMainService.getWindowById(windowId); + + if (codeWindow) { + codeWindow.win.minimize(); + } + + return TPromise.as(null); + } + onWindowTitleDoubleClick(windowId: number): TPromise { this.logService.trace('windowsService#onWindowTitleDoubleClick', windowId); const codeWindow = this.windowsMainService.getWindowById(windowId); @@ -360,7 +399,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { this.logService.trace('windowsService#openWindow'); if (!paths || !paths.length) { return TPromise.as(null); @@ -369,8 +408,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable this.windowsMainService.open({ context: OpenContext.API, contextWindowId: windowId, - cli: this.environmentService.args, - pathsToOpen: paths, + urisToOpen: paths, + cli: options && options.args ? { ...this.environmentService.args, ...options.args } : this.environmentService.args, forceNewWindow: options && options.forceNewWindow, forceReuseWindow: options && options.forceReuseWindow, forceOpenWorkspaceAsFile: options && options.forceOpenWorkspaceAsFile @@ -381,7 +420,9 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable openNewWindow(): TPromise { this.logService.trace('windowsService#openNewWindow'); + this.windowsMainService.openNewWindow(OpenContext.API); + return TPromise.as(null); } @@ -396,10 +437,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { this.logService.trace('windowsService#getWindows'); const windows = this.windowsMainService.getWindows(); - const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, openedFolderPath: w.openedFolderPath, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); + const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, folderUri: w.openedFolderUri, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); return TPromise.as(result); } @@ -420,6 +461,10 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + getActiveWindowId(): TPromise { + return TPromise.as(this._activeWindowId); + } + openExternal(url: string): TPromise { this.logService.trace('windowsService#openExternal'); return TPromise.as(shell.openExternal(url)); @@ -459,14 +504,21 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable this.logService.trace('windowsService#openAboutDialog'); const lastActiveWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + let version = app.getVersion(); + + if (product.target) { + version = `${version} (${product.target} setup)`; + } + const detail = nls.localize('aboutDetail', - "Version {0}\nCommit {1}\nDate {2}\nShell {3}\nRenderer {4}\nNode {5}\nArchitecture {6}", - app.getVersion(), + "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nArchitecture: {7}", + version, product.commit || 'Unknown', product.date || 'Unknown', process.versions['electron'], process.versions['chrome'], process.versions['node'], + process.versions['v8'], process.arch ); @@ -491,24 +543,25 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { - return this.openFileForURI(URI.file(uri.fsPath)); + this.openFileForURI(URI.file(uri.fsPath)); + return TPromise.as(true); } - return false; + return TPromise.wrap(false); } - private async openFileForURI(uri: URI): TPromise { + private openFileForURI(uri: URI): TPromise { const cli = assign(Object.create(null), this.environmentService.args, { goto: true }); - const pathsToOpen = [uri.fsPath]; + const urisToOpen = [uri]; - this.windowsMainService.open({ context: OpenContext.API, cli, pathsToOpen }); - return true; + this.windowsMainService.open({ context: OpenContext.API, cli, urisToOpen }); + return TPromise.wrap(true); } dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/node/windowsIpc.ts new file mode 100644 index 00000000000..f5546a01615 --- /dev/null +++ b/src/vs/platform/windows/node/windowsIpc.ts @@ -0,0 +1,409 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { Event, buffer } from 'vs/base/common/event'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions } from 'vs/platform/windows/common/windows'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, 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, UriComponents } from 'vs/base/common/uri'; +import { ParsedArgs } from 'vs/platform/environment/common/environment'; + +export interface IWindowsChannel extends IChannel { + listen(event: 'onWindowOpen'): Event; + listen(event: 'onWindowFocus'): Event; + listen(event: 'onWindowBlur'): Event; + listen(event: 'onWindowMaximize'): Event; + listen(event: 'onWindowUnmaximize'): Event; + listen(event: 'onRecentlyOpenedChange'): Event; + listen(event: string, arg?: any): Event; + + call(command: 'pickFileFolderAndOpen', arg: INativeOpenDialogOptions): Thenable; + call(command: 'pickFileAndOpen', arg: INativeOpenDialogOptions): Thenable; + call(command: 'pickFolderAndOpen', arg: INativeOpenDialogOptions): Thenable; + call(command: 'pickWorkspaceAndOpen', arg: INativeOpenDialogOptions): Thenable; + call(command: 'showMessageBox', arg: [number, MessageBoxOptions]): Thenable; + call(command: 'showSaveDialog', arg: [number, SaveDialogOptions]): Thenable; + call(command: 'showOpenDialog', arg: [number, OpenDialogOptions]): Thenable; + call(command: 'reloadWindow', arg: [number, ParsedArgs]): Thenable; + call(command: 'openDevTools', arg: [number, IDevToolsOptions]): Thenable; + call(command: 'toggleDevTools', arg: number): Thenable; + call(command: 'closeWorkspace', arg: number): Thenable; + call(command: 'enterWorkspace', arg: [number, string]): Thenable; + call(command: 'createAndEnterWorkspace', arg: [number, IWorkspaceFolderCreationData[], string]): Thenable; + call(command: 'saveAndEnterWorkspace', arg: [number, string]): Thenable; + call(command: 'toggleFullScreen', arg: number): Thenable; + call(command: 'setRepresentedFilename', arg: [number, string]): Thenable; + call(command: 'addRecentlyOpened', arg: UriComponents[]): Thenable; + call(command: 'removeFromRecentlyOpened', arg: (IWorkspaceIdentifier | UriComponents | string)[]): Thenable; + call(command: 'clearRecentlyOpened'): Thenable; + call(command: 'getRecentlyOpened', arg: number): Thenable; + call(command: 'newWindowTab'): Thenable; + call(command: 'showPreviousWindowTab'): Thenable; + call(command: 'showNextWindowTab'): Thenable; + call(command: 'moveWindowTabToNewWindow'): Thenable; + call(command: 'mergeAllWindowTabs'): Thenable; + call(command: 'toggleWindowTabsBar'): Thenable; + call(command: 'updateTouchBar', arg: [number, ISerializableCommandAction[][]]): Thenable; + call(command: 'focusWindow', arg: number): Thenable; + call(command: 'closeWindow', arg: number): Thenable; + call(command: 'isFocused', arg: number): Thenable; + call(command: 'isMaximized', arg: number): Thenable; + call(command: 'maximizeWindow', arg: number): Thenable; + call(command: 'unmaximizeWindow', arg: number): Thenable; + call(command: 'minimizeWindow', arg: number): Thenable; + call(command: 'onWindowTitleDoubleClick', arg: number): Thenable; + call(command: 'setDocumentEdited', arg: [number, boolean]): Thenable; + call(command: 'quit'): Thenable; + call(command: 'openWindow', arg: [number, URI[], { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }]): Thenable; + call(command: 'openNewWindow'): Thenable; + call(command: 'showWindow', arg: number): Thenable; + call(command: 'getWindows'): Thenable<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; + call(command: 'getWindowCount'): Thenable; + call(command: 'relaunch', arg: [{ addArgs?: string[], removeArgs?: string[] }]): Thenable; + call(command: 'whenSharedProcessReady'): Thenable; + call(command: 'toggleSharedProcess'): Thenable; + call(command: 'log', arg: [string, string[]]): Thenable; + call(command: 'showItemInFolder', arg: string): Thenable; + call(command: 'getActiveWindowId'): Thenable; + call(command: 'openExternal', arg: string): Thenable; + call(command: 'startCrashReporter', arg: CrashReporterStartOptions): Thenable; + call(command: 'openAboutDialog'): Thenable; +} + +export class WindowsChannel implements IWindowsChannel { + + private onWindowOpen: Event; + private onWindowFocus: Event; + private onWindowBlur: Event; + private onWindowMaximize: Event; + private onWindowUnmaximize: Event; + private onRecentlyOpenedChange: Event; + + constructor(private service: IWindowsService) { + this.onWindowOpen = buffer(service.onWindowOpen, true); + this.onWindowFocus = buffer(service.onWindowFocus, true); + this.onWindowBlur = buffer(service.onWindowBlur, true); + this.onWindowMaximize = buffer(service.onWindowMaximize, true); + this.onWindowUnmaximize = buffer(service.onWindowUnmaximize, true); + this.onRecentlyOpenedChange = buffer(service.onRecentlyOpenedChange, true); + } + + listen(event: string, arg?: any): Event { + switch (event) { + case 'onWindowOpen': return this.onWindowOpen; + case 'onWindowFocus': return this.onWindowFocus; + case 'onWindowBlur': return this.onWindowBlur; + case 'onWindowMaximize': return this.onWindowMaximize; + case 'onWindowUnmaximize': return this.onWindowUnmaximize; + case 'onRecentlyOpenedChange': return this.onRecentlyOpenedChange; + } + + throw new Error('No event found'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg); + case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg); + case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg); + case 'pickWorkspaceAndOpen': return this.service.pickWorkspaceAndOpen(arg); + case 'showMessageBox': return this.service.showMessageBox(arg[0], arg[1]); + case 'showSaveDialog': return this.service.showSaveDialog(arg[0], arg[1]); + case 'showOpenDialog': return this.service.showOpenDialog(arg[0], arg[1]); + case 'reloadWindow': return this.service.reloadWindow(arg[0], arg[1]); + 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[]; + 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 '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)); + case 'removeFromRecentlyOpened': { + let paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string)[] = arg; + if (Array.isArray(paths)) { + paths = paths.map(path => isWorkspaceIdentifier(path) ? path : URI.revive(path)); + } + return this.service.removeFromRecentlyOpened(paths); + } + case 'clearRecentlyOpened': return this.service.clearRecentlyOpened(); + case 'newWindowTab': return this.service.newWindowTab(); + case 'showPreviousWindowTab': return this.service.showPreviousWindowTab(); + case 'showNextWindowTab': return this.service.showNextWindowTab(); + case 'moveWindowTabToNewWindow': return this.service.moveWindowTabToNewWindow(); + case 'mergeAllWindowTabs': return this.service.mergeAllWindowTabs(); + case 'toggleWindowTabsBar': return this.service.toggleWindowTabsBar(); + case 'updateTouchBar': return this.service.updateTouchBar(arg[0], arg[1]); + case 'getRecentlyOpened': return this.service.getRecentlyOpened(arg); + case 'focusWindow': return this.service.focusWindow(arg); + case 'closeWindow': return this.service.closeWindow(arg); + case 'isFocused': return this.service.isFocused(arg); + case 'isMaximized': return this.service.isMaximized(arg); + case 'maximizeWindow': return this.service.maximizeWindow(arg); + case 'unmaximizeWindow': return this.service.unmaximizeWindow(arg); + case 'minimizeWindow': return this.service.minimizeWindow(arg); + case 'onWindowTitleDoubleClick': return this.service.onWindowTitleDoubleClick(arg); + case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); + case 'openWindow': return this.service.openWindow(arg[0], arg[1] ? (arg[1]).map(r => URI.revive(r)) : arg[1], arg[2]); + case 'openNewWindow': return this.service.openNewWindow(); + case 'showWindow': return this.service.showWindow(arg); + case 'getWindows': return this.service.getWindows(); + case 'getWindowCount': return this.service.getWindowCount(); + case 'relaunch': return this.service.relaunch(arg[0]); + case 'whenSharedProcessReady': return this.service.whenSharedProcessReady(); + case 'toggleSharedProcess': return this.service.toggleSharedProcess(); + case 'quit': return this.service.quit(); + case 'log': return this.service.log(arg[0], arg[1]); + case 'showItemInFolder': return this.service.showItemInFolder(arg); + case 'getActiveWindowId': return this.service.getActiveWindowId(); + case 'openExternal': return this.service.openExternal(arg); + case 'startCrashReporter': return this.service.startCrashReporter(arg); + case 'openAboutDialog': return this.service.openAboutDialog(); + } + return undefined; + } +} + +export class WindowsChannelClient implements IWindowsService { + + _serviceBrand: any; + + constructor(private channel: IWindowsChannel) { } + + get onWindowOpen(): Event { return this.channel.listen('onWindowOpen'); } + get onWindowFocus(): Event { return this.channel.listen('onWindowFocus'); } + get onWindowBlur(): Event { return this.channel.listen('onWindowBlur'); } + get onWindowMaximize(): Event { return this.channel.listen('onWindowMaximize'); } + get onWindowUnmaximize(): Event { return this.channel.listen('onWindowUnmaximize'); } + get onRecentlyOpenedChange(): Event { return this.channel.listen('onRecentlyOpenedChange'); } + + pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('pickFileFolderAndOpen', options)); + } + + pickFileAndOpen(options: INativeOpenDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('pickFileAndOpen', options)); + } + + pickFolderAndOpen(options: INativeOpenDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('pickFolderAndOpen', options)); + } + + pickWorkspaceAndOpen(options: INativeOpenDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('pickWorkspaceAndOpen', options)); + } + + showMessageBox(windowId: number, options: MessageBoxOptions): TPromise { + return TPromise.wrap(this.channel.call('showMessageBox', [windowId, options])); + } + + showSaveDialog(windowId: number, options: SaveDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('showSaveDialog', [windowId, options])); + } + + showOpenDialog(windowId: number, options: OpenDialogOptions): TPromise { + return TPromise.wrap(this.channel.call('showOpenDialog', [windowId, options])); + } + + reloadWindow(windowId: number, args?: ParsedArgs): TPromise { + return TPromise.wrap(this.channel.call('reloadWindow', [windowId, args])); + } + + openDevTools(windowId: number, options?: IDevToolsOptions): TPromise { + return TPromise.wrap(this.channel.call('openDevTools', [windowId, options])); + } + + toggleDevTools(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('toggleDevTools', windowId)); + } + + closeWorkspace(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('closeWorkspace', windowId)); + } + + enterWorkspace(windowId: number, path: string): TPromise { + return TPromise.wrap(this.channel.call('enterWorkspace', [windowId, path])); + } + + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { + return TPromise.wrap(this.channel.call('createAndEnterWorkspace', [windowId, folders, path])); + } + + saveAndEnterWorkspace(windowId: number, path: string): TPromise { + return TPromise.wrap(this.channel.call('saveAndEnterWorkspace', [windowId, path])); + } + + toggleFullScreen(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('toggleFullScreen', windowId)); + } + + setRepresentedFilename(windowId: number, fileName: string): TPromise { + return TPromise.wrap(this.channel.call('setRepresentedFilename', [windowId, fileName])); + } + + addRecentlyOpened(files: URI[]): TPromise { + return TPromise.wrap(this.channel.call('addRecentlyOpened', files)); + } + + removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI)[]): TPromise { + return TPromise.wrap(this.channel.call('removeFromRecentlyOpened', paths)); + } + + clearRecentlyOpened(): TPromise { + return TPromise.wrap(this.channel.call('clearRecentlyOpened')); + } + + getRecentlyOpened(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('getRecentlyOpened', windowId)) + .then(recentlyOpened => { + recentlyOpened.workspaces = recentlyOpened.workspaces.map(workspace => isWorkspaceIdentifier(workspace) ? workspace : URI.revive(workspace)); + recentlyOpened.files = recentlyOpened.files.map(URI.revive); + return recentlyOpened; + }); + } + + newWindowTab(): TPromise { + return TPromise.wrap(this.channel.call('newWindowTab')); + } + + showPreviousWindowTab(): TPromise { + return TPromise.wrap(this.channel.call('showPreviousWindowTab')); + } + + showNextWindowTab(): TPromise { + return TPromise.wrap(this.channel.call('showNextWindowTab')); + } + + moveWindowTabToNewWindow(): TPromise { + return TPromise.wrap(this.channel.call('moveWindowTabToNewWindow')); + } + + mergeAllWindowTabs(): TPromise { + return TPromise.wrap(this.channel.call('mergeAllWindowTabs')); + } + + toggleWindowTabsBar(): TPromise { + return TPromise.wrap(this.channel.call('toggleWindowTabsBar')); + } + + focusWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('focusWindow', windowId)); + } + + closeWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('closeWindow', windowId)); + } + + isFocused(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('isFocused', windowId)); + } + + isMaximized(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('isMaximized', windowId)); + } + + maximizeWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('maximizeWindow', windowId)); + } + + unmaximizeWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('unmaximizeWindow', windowId)); + } + + minimizeWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('minimizeWindow', windowId)); + } + + onWindowTitleDoubleClick(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('onWindowTitleDoubleClick', windowId)); + } + + setDocumentEdited(windowId: number, flag: boolean): TPromise { + return TPromise.wrap(this.channel.call('setDocumentEdited', [windowId, flag])); + } + + quit(): TPromise { + return TPromise.wrap(this.channel.call('quit')); + } + + relaunch(options: { addArgs?: string[], removeArgs?: string[] }): TPromise { + return TPromise.wrap(this.channel.call('relaunch', [options])); + } + + whenSharedProcessReady(): TPromise { + return TPromise.wrap(this.channel.call('whenSharedProcessReady')); + } + + toggleSharedProcess(): TPromise { + return TPromise.wrap(this.channel.call('toggleSharedProcess')); + } + + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): TPromise { + return TPromise.wrap(this.channel.call('openWindow', [windowId, paths, options])); + } + + openNewWindow(): TPromise { + return TPromise.wrap(this.channel.call('openNewWindow')); + } + + showWindow(windowId: number): TPromise { + return TPromise.wrap(this.channel.call('showWindow', windowId)); + } + + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { + return TPromise.wrap(this.channel.call('getWindows').then(result => { result.forEach(win => win.folderUri = win.folderUri ? URI.revive(win.folderUri) : win.folderUri); return result; })); + } + + getWindowCount(): TPromise { + return TPromise.wrap(this.channel.call('getWindowCount')); + } + + log(severity: string, ...messages: string[]): TPromise { + return TPromise.wrap(this.channel.call('log', [severity, messages])); + } + + showItemInFolder(path: string): TPromise { + return TPromise.wrap(this.channel.call('showItemInFolder', path)); + } + + getActiveWindowId(): TPromise { + return TPromise.wrap(this.channel.call('getActiveWindowId')); + } + + openExternal(url: string): TPromise { + return TPromise.wrap(this.channel.call('openExternal', url)); + } + + startCrashReporter(config: CrashReporterStartOptions): TPromise { + return TPromise.wrap(this.channel.call('startCrashReporter', config)); + } + + updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): TPromise { + return TPromise.wrap(this.channel.call('updateTouchBar', [windowId, items])); + } + + openAboutDialog(): TPromise { + return TPromise.wrap(this.channel.call('openAboutDialog')); + } +} diff --git a/src/vs/platform/workbench/common/contextkeys.ts b/src/vs/platform/workbench/common/contextkeys.ts index c4a740d8c28..d513b3e6c91 100644 --- a/src/vs/platform/workbench/common/contextkeys.ts +++ b/src/vs/platform/workbench/common/contextkeys.ts @@ -6,6 +6,11 @@ 'use strict'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; export const InputFocusedContextKey = 'inputFocus'; -export const InputFocusedContext = new RawContextKey(InputFocusedContextKey, false); \ No newline at end of file +export const InputFocusedContext = new RawContextKey(InputFocusedContextKey, false); +export const FileDialogContext = new RawContextKey('fileDialog', 'local'); +export const IsMacContext = new RawContextKey('isMac', isMacintosh); +export const IsLinuxContext = new RawContextKey('isLinux', isLinux); +export const IsWindowsContext = new RawContextKey('isWindows', isWindows); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index b9b31f4b2a0..78178fb4e68 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TernarySearchTree } from 'vs/base/common/map'; import { Event } from 'vs/base/common/event'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { isLinux } from 'vs/base/common/platform'; export const IWorkspaceContextService = createDecorator('contextService'); -export enum WorkbenchState { +export const enum WorkbenchState { EMPTY = 1, FOLDER, WORKSPACE @@ -77,6 +77,14 @@ export interface IWorkspaceContextService { isInsideWorkspace(resource: URI): boolean; } +export namespace IWorkspace { + export function isIWorkspace(thing: any): thing is IWorkspace { + return thing && typeof thing === 'object' + && typeof (thing as IWorkspace).id === 'string' + && Array.isArray((thing as IWorkspace).folders); + } +} + export interface IWorkspace { /** @@ -84,11 +92,6 @@ export interface IWorkspace { */ readonly id: string; - /** - * the name of the workspace. - */ - readonly name: string; - /** * Folders in the workspace. */ @@ -118,6 +121,15 @@ export interface IWorkspaceFolderData { readonly index: number; } +export namespace IWorkspaceFolder { + export function isIWorkspaceFolder(thing: any): thing is IWorkspaceFolder { + return thing && typeof thing === 'object' + && URI.isUri((thing as IWorkspaceFolder).uri) + && typeof (thing as IWorkspaceFolder).name === 'string' + && typeof (thing as IWorkspaceFolder).toResource === 'function'; + } +} + export interface IWorkspaceFolder extends IWorkspaceFolderData { /** @@ -133,7 +145,6 @@ export class Workspace implements IWorkspace { constructor( private _id: string, - private _name: string = '', folders: WorkspaceFolder[] = [], private _configuration: URI = null, private _ctime?: number @@ -141,48 +152,39 @@ export class Workspace implements IWorkspace { this.folders = folders; } - public update(workspace: Workspace) { + update(workspace: Workspace) { this._id = workspace.id; - this._name = workspace.name; this._configuration = workspace.configuration; this._ctime = workspace.ctime; this.folders = workspace.folders; } - public get folders(): WorkspaceFolder[] { + get folders(): WorkspaceFolder[] { return this._folders; } - public set folders(folders: WorkspaceFolder[]) { + set folders(folders: WorkspaceFolder[]) { this._folders = folders; this.updateFoldersMap(); } - public get id(): string { + get id(): string { return this._id; } - public get ctime(): number { + get ctime(): number { return this._ctime; } - public get name(): string { - return this._name; - } - - public set name(name: string) { - this._name = name; - } - - public get configuration(): URI { + get configuration(): URI { return this._configuration; } - public set configuration(configuration: URI) { + set configuration(configuration: URI) { this._configuration = configuration; } - public getFolder(resource: URI): IWorkspaceFolder { + getFolder(resource: URI): IWorkspaceFolder { if (!resource) { return null; } @@ -197,8 +199,8 @@ export class Workspace implements IWorkspace { } } - public toJSON(): IWorkspace { - return { id: this.id, folders: this.folders, name: this.name, configuration: this.configuration }; + toJSON(): IWorkspace { + return { id: this.id, folders: this.folders, configuration: this.configuration }; } } @@ -216,7 +218,7 @@ export class WorkspaceFolder implements IWorkspaceFolder { } toResource(relativePath: string): URI { - return this.uri.with({ path: paths.join(this.uri.path, relativePath) }); + return resources.joinPath(this.uri, relativePath); } toJSON(): IWorkspaceFolderData { @@ -260,7 +262,7 @@ function toUri(path: string, relativeTo: URI): URI { return URI.file(path); } if (relativeTo) { - return relativeTo.with({ path: paths.join(relativeTo.path, path) }); + return resources.joinPath(relativeTo, path); } } return null; diff --git a/src/vs/platform/workspace/test/common/testWorkspace.ts b/src/vs/platform/workspace/test/common/testWorkspace.ts index 6a204873e0c..2378eca9c79 100644 --- a/src/vs/platform/workspace/test/common/testWorkspace.ts +++ b/src/vs/platform/workspace/test/common/testWorkspace.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { isWindows } from 'vs/base/common/platform'; @@ -13,7 +13,6 @@ export const TestWorkspace = testWorkspace(wsUri); export function testWorkspace(resource: URI): Workspace { return new Workspace( resource.toString(), - resource.fsPath, toWorkspaceFolders([{ path: resource.fsPath }]) ); -} \ No newline at end of file +} diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index ac6f0caf17f..c4c25fd4932 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -7,14 +7,14 @@ import * as assert from 'assert'; import { Workspace, toWorkspaceFolders, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; suite('Workspace', () => { test('getFolder returns the folder with given uri', () => { const expected = new WorkspaceFolder({ uri: URI.file('/src/test'), name: '', index: 2 }); - let testObject = new Workspace('', '', [new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 0 }), expected, new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })]); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 0 }), expected, new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })]); const actual = testObject.getFolder(expected.uri); @@ -23,7 +23,7 @@ suite('Workspace', () => { test('getFolder returns the folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: URI.file('/src/test'), name: '', index: 0 }); - let testObject = new Workspace('', '', [expected, new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 1 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })]); + let testObject = new Workspace('', [expected, new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 1 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })]); const actual = testObject.getFolder(URI.file('/src/test/a')); @@ -32,7 +32,7 @@ suite('Workspace', () => { test('getFolder returns the closest folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: URI.file('/src/test'), name: '', index: 2 }); - let testObject = new Workspace('', '', [new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected]); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: URI.file('/src/main'), name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected]); const actual = testObject.getFolder(URI.file('/src/test/a')); @@ -40,7 +40,7 @@ suite('Workspace', () => { }); test('getFolder returns null if the uri is not sub', () => { - let testObject = new Workspace('', '', [new WorkspaceFolder({ uri: URI.file('/src/test'), name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 })]); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: URI.file('/src/test'), name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 })]); const actual = testObject.getFolder(URI.file('/src/main/a')); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d161a54cf90..656d04085ba 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -7,15 +7,10 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import { isParent } from 'vs/platform/files/common/files'; import { localize } from 'vs/nls'; -import { basename, dirname, join } from 'vs/base/common/paths'; -import { isLinux } from 'vs/base/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Event } from 'vs/base/common/event'; -import { tildify, getPathLabel } from 'vs/base/common/labels'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import URI from 'vs/base/common/uri'; +import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { URI } from 'vs/base/common/uri'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); export const IWorkspacesService = createDecorator('workspacesService'); @@ -27,7 +22,7 @@ export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; /** * A single folder workspace identifier is just the path to the folder. */ -export type ISingleFolderWorkspaceIdentifier = string; +export type ISingleFolderWorkspaceIdentifier = URI; export interface IWorkspaceIdentifier { id: string; @@ -92,6 +87,8 @@ export interface IWorkspacesMainService extends IWorkspacesService { createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; + resolveWorkspace(path: string): TPromise; + resolveWorkspaceSync(path: string): IResolvedWorkspace; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; @@ -109,30 +106,8 @@ export interface IWorkspacesService { createWorkspace(folders?: IWorkspaceFolderCreationData[]): TPromise; } -export function getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier), environmentService: IEnvironmentService, options?: { verbose: boolean }): string { - - // Workspace: Single Folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - return tildify(workspace, environmentService.userHome); - } - - // Workspace: Untitled - if (isParent(workspace.configPath, environmentService.workspacesHome, !isLinux /* ignore case */)) { - return localize('untitledWorkspace', "Untitled (Workspace)"); - } - - // Workspace: Saved - const filename = basename(workspace.configPath); - const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); - if (options && options.verbose) { - return localize('workspaceNameVerbose', "{0} (Workspace)", getPathLabel(join(dirname(workspace.configPath), workspaceName), null, environmentService)); - } - - return localize('workspaceName', "{0} (Workspace)", workspaceName); -} - export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { - return typeof obj === 'string'; + return obj instanceof URI; } export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { @@ -140,3 +115,18 @@ export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && typeof workspaceIdentifier.configPath === 'string'; } + +export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + if (workspace.configuration) { + return { + configPath: workspace.configuration.fsPath, + id: workspace.id + }; + } + if (workspace.folders.length === 1) { + return workspace.folders[0].uri; + } + + // Empty workspace + return undefined; +} diff --git a/src/vs/platform/workspaces/common/workspacesIpc.ts b/src/vs/platform/workspaces/common/workspacesIpc.ts deleted file mode 100644 index 91257d6d12e..00000000000 --- a/src/vs/platform/workspaces/common/workspacesIpc.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWorkspacesService, IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; -import URI from 'vs/base/common/uri'; - -export interface IWorkspacesChannel extends IChannel { - call(command: 'createWorkspace', arg: [IWorkspaceFolderCreationData[]]): TPromise; - call(command: string, arg?: any): TPromise; -} - -export class WorkspacesChannel implements IWorkspacesChannel { - - constructor(private service: IWorkspacesMainService) { } - - public call(command: string, arg?: any): TPromise { - switch (command) { - case 'createWorkspace': { - const rawFolders: IWorkspaceFolderCreationData[] = arg; - let folders: IWorkspaceFolderCreationData[]; - 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.createWorkspace(folders); - } - } - - return void 0; - } -} - -export class WorkspacesChannelClient implements IWorkspacesService { - - _serviceBrand: any; - - constructor(private channel: IWorkspacesChannel) { } - - public createWorkspace(folders?: IWorkspaceFolderCreationData[]): TPromise { - return this.channel.call('createWorkspace', folders); - } -} \ No newline at end of file diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index a339d3efafb..c91781b2987 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -24,7 +24,7 @@ import * as jsonEdit from 'vs/base/common/jsonEdit'; import { applyEdit } from 'vs/base/common/jsonFormatter'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; export interface IStoredWorkspace { @@ -33,7 +33,7 @@ export interface IStoredWorkspace { export class WorkspacesMainService implements IWorkspacesMainService { - public _serviceBrand: any; + _serviceBrand: any; protected workspacesHome: string; @@ -50,15 +50,23 @@ export class WorkspacesMainService implements IWorkspacesMainService { this._onUntitledWorkspaceDeleted = new Emitter(); } - public get onWorkspaceSaved(): Event { + get onWorkspaceSaved(): Event { return this._onWorkspaceSaved.event; } - public get onUntitledWorkspaceDeleted(): Event { + get onUntitledWorkspaceDeleted(): Event { return this._onUntitledWorkspaceDeleted.event; } - public resolveWorkspaceSync(path: string): IResolvedWorkspace { + resolveWorkspace(path: string): TPromise { + if (!this.isWorkspacePath(path)) { + return TPromise.as(null); // does not look like a valid workspace config file + } + + return readFile(path, 'utf8').then(contents => this.doResolveWorkspace(path, contents)); + } + + resolveWorkspaceSync(path: string): IResolvedWorkspace { if (!this.isWorkspacePath(path)) { return null; // does not look like a valid workspace config file } @@ -96,12 +104,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace { // Parse workspace file - let storedWorkspace: IStoredWorkspace; - try { - storedWorkspace = json.parse(contents); // use fault tolerant parser - } catch (error) { - throw new Error(`${path} cannot be parsed as JSON file (${error}).`); - } + 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)) { @@ -120,7 +123,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */); } - public createWorkspace(folders?: IWorkspaceFolderCreationData[]): TPromise { + createWorkspace(folders?: IWorkspaceFolderCreationData[]): TPromise { const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders); return mkdirp(configParent).then(() => { @@ -128,7 +131,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { }); } - public createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier { + createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier { const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders); if (!existsSync(this.workspacesHome)) { @@ -180,7 +183,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { }; } - public getWorkspaceId(workspaceConfigPath: string): string { + getWorkspaceId(workspaceConfigPath: string): string { if (!isLinux) { workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system } @@ -188,11 +191,11 @@ export class WorkspacesMainService implements IWorkspacesMainService { return createHash('md5').update(workspaceConfigPath).digest('hex'); } - public isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { return this.isInsideWorkspacesHome(workspace.configPath); } - public saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise { + saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise { // Return early if target is same as source if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) { @@ -247,7 +250,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { }); } - public deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { if (!this.isUntitledWorkspace(workspace)) { return; // only supported for untitled workspaces } @@ -267,7 +270,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { } } - public getUntitledWorkspacesSync(): IWorkspaceIdentifier[] { + getUntitledWorkspacesSync(): IWorkspaceIdentifier[] { let untitledWorkspacePaths: string[] = []; try { untitledWorkspacePaths = readdirSync(this.workspacesHome).map(folder => join(this.workspacesHome, folder, UNTITLED_WORKSPACE_NAME)); diff --git a/src/vs/platform/workspaces/node/workspacesIpc.ts b/src/vs/platform/workspaces/node/workspacesIpc.ts new file mode 100644 index 00000000000..6ef88453c47 --- /dev/null +++ b/src/vs/platform/workspaces/node/workspacesIpc.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IWorkspacesService, IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; + +export interface IWorkspacesChannel extends IChannel { + call(command: 'createWorkspace', arg: [IWorkspaceFolderCreationData[]]): Thenable; + call(command: string, arg?: any): Thenable; +} + +export class WorkspacesChannel implements IWorkspacesChannel { + + constructor(private service: IWorkspacesMainService) { } + + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + + call(command: string, arg?: any): Thenable { + switch (command) { + case 'createWorkspace': { + const rawFolders: IWorkspaceFolderCreationData[] = arg; + let folders: IWorkspaceFolderCreationData[]; + 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.createWorkspace(folders); + } + } + + return void 0; + } +} + +export class WorkspacesChannelClient implements IWorkspacesService { + + _serviceBrand: any; + + constructor(private channel: IWorkspacesChannel) { } + + createWorkspace(folders?: IWorkspaceFolderCreationData[]): TPromise { + return TPromise.wrap(this.channel.call('createWorkspace', folders)); + } +} \ No newline at end of file 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 210d5253765..36d5df9557e 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -16,7 +16,7 @@ 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 { NullLogService } from 'vs/platform/log/common/log'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; @@ -366,14 +366,22 @@ suite('WorkspacesMainService', () => { assert.equal(0, untitled.length); return createWorkspace([process.cwd(), os.tmpdir()]).then(untitledOne => { + assert.ok(fs.existsSync(untitledOne.configPath)); + untitled = service.getUntitledWorkspacesSync(); assert.equal(1, untitled.length); assert.equal(untitledOne.id, untitled[0].id); return createWorkspace([os.tmpdir(), process.cwd()]).then(untitledTwo => { + assert.ok(fs.existsSync(untitledTwo.configPath)); + untitled = service.getUntitledWorkspacesSync(); + if (untitled.length === 1) { + assert.fail('Unexpected workspaces count, contents:\n' + fs.readFileSync(untitledTwo.configPath, 'utf8')); + } + assert.equal(2, untitled.length); service.deleteUntitledWorkspaceSync(untitledOne); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index c16e2810bca..c1f073939fe 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -754,7 +754,8 @@ declare module 'vscode' { * An optional view column in which the [editor](#TextEditor) should be shown. * The default is the [active](#ViewColumn.Active), other values are adjusted to * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is - * not adjusted. + * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the + * editor to the side of the currently active one. */ viewColumn?: ViewColumn; @@ -1230,24 +1231,41 @@ declare module 'vscode' { */ export class Uri { + /** + * Create an URI from a string, e.g. `http://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @see [Uri.toString](#Uri.toString) + * @param value The string value of an Uri. + * @return A new Uri instance. + */ + static parse(value: string): Uri; + /** * Create an URI from a file system path. The [scheme](#Uri.scheme) * will be `file`. + * + * The *difference* between `Uri#parse` and `Uri#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `Uri.file(path)` is *not* the same as + * `Uri.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = URI.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + + const bad = URI.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` * * @param path A file system or UNC path. * @return A new Uri instance. */ static file(path: string): Uri; - /** - * Create an URI from a string. Will throw if the given value is not - * valid. - * - * @param value The string value of an Uri. - * @return A new Uri instance. - */ - static parse(value: string): Uri; - /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ @@ -1284,8 +1302,21 @@ declare module 'vscode' { * The string representing the corresponding file system path of this Uri. * * Will handle UNC paths and normalize windows drive letters to lower-case. Also - * uses the platform specific path separator. Will *not* validate the path for - * invalid characters and semantics. Will *not* look at the scheme of this Uri. + * uses the platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this Uri. + * * The resulting string shall *not* be used for display purposes but + * for disk operations, like `readFile` et al. + * + * The *difference* to the [`path`](#Uri.path)-property is the use of the platform specific + * path separator and the handling of UNC paths. The sample below outlines the difference: + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` */ readonly fsPath: string; @@ -1307,8 +1338,10 @@ declare module 'vscode' { /** * Returns a string representation of this Uri. The representation and normalization - * of a URI depends on the scheme. The resulting string can be safely used with - * [Uri.parse](#Uri.parse). + * of a URI depends on the scheme. + * + * * The resulting string can be safely used with [Uri.parse](#Uri.parse). + * * The resulting string shall *not* be used for display purposes. * * @param skipEncoding Do not percentage-encode the result, defaults to `false`. Note that * the `#` and `?` characters occurring in the path will always be encoded. @@ -2155,6 +2188,41 @@ declare module 'vscode' { resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; } + /** + * Information about where a symbol is defined. + * + * Provides additional metadata over normal [location](#Location) definitions, including the range of + * the defining symbol + */ + export interface DefinitionLink { + /** + * Span of the symbol being defined in the source file. + * + * Used as the underlined span for mouse definition hover. Defaults to the word range at + * the definition position. + */ + originSelectionRange?: Range; + + /** + * The resource identifier of the definition. + */ + targetUri: Uri; + + /** + * The full range of the definition. + * + * For a class definition for example, this would be the entire body of the class definition. + */ + targetRange: Range; + + /** + * The span of the symbol definition. + * + * For a class definition, this would be the class name itself in the class definition. + */ + targetSelectionRange?: Range; + } + /** * The definition of a symbol represented as one or many [locations](#Location). * For most programming languages there is only one location at which a symbol is @@ -2178,7 +2246,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2196,7 +2264,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2214,7 +2282,7 @@ declare module 'vscode' { * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -2447,7 +2515,7 @@ declare module 'vscode' { * @param name The name of the symbol. * @param kind The kind of the symbol. * @param containerName The name of the symbol containing the symbol. - * @param location The the location of the symbol. + * @param location The location of the symbol. */ constructor(name: string, kind: SymbolKind, containerName: string, location: Location); @@ -2465,6 +2533,56 @@ declare module 'vscode' { constructor(name: string, kind: SymbolKind, range: Range, uri?: Uri, containerName?: string); } + /** + * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document + * symbols can be hierarchical and they have two ranges: one that encloses its definition and one that points to + * its most interesting range, e.g. the range of an identifier. + */ + export class DocumentSymbol { + + /** + * The name of this symbol. + */ + name: string; + + /** + * More detail for this symbol, e.g the signature of a function. + */ + detail: string; + + /** + * The kind of this symbol. + */ + kind: SymbolKind; + + /** + * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g comments and code. + */ + range: Range; + + /** + * The range that should be selected and reveal when this symbol is being picked, e.g the name of a function. + * Must be contained by the [`range`](#DocumentSymbol.range). + */ + selectionRange: Range; + + /** + * Children of this symbol, e.g. properties of a class. + */ + children: DocumentSymbol[]; + + /** + * Creates a new document symbol. + * + * @param name The name of the symbol. + * @param detail Details for the symbol. + * @param kind The kind of the symbol. + * @param range The full range of the symbol. + * @param selectionRange The range that should be reveal. + */ + constructor(name: string, detail: string, kind: SymbolKind, range: Range, selectionRange: Range); + } + /** * The document symbol provider interface defines the contract between extensions and * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-symbol)-feature. @@ -2479,7 +2597,7 @@ declare module 'vscode' { * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult; + provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult; } /** @@ -2489,16 +2607,17 @@ declare module 'vscode' { export interface WorkspaceSymbolProvider { /** - * Project-wide search for a symbol matching the given query string. It is up to the provider - * how to search given the query string, like substring, indexOf etc. To improve performance implementors can - * skip the [location](#SymbolInformation.location) of symbols and implement `resolveWorkspaceSymbol` to do that - * later. + * Project-wide search for a symbol matching the given query string. * * The `query`-parameter should be interpreted in a *relaxed way* as the editor will apply its own highlighting * and scoring on the results. A good rule of thumb is to match case-insensitive and to simply check that the * characters of *query* appear in their order in a candidate symbol. Don't use prefix, substring, or similar * strict matching. * + * To improve performance implementors can implement `resolveWorkspaceSymbol` and then provide symbols with partial + * [location](#SymbolInformation.location)-objects, without a `range` defined. The editor will then call + * `resolveWorkspaceSymbol` for selected symbols only, e.g. when opening a workspace symbol. + * * @param query A non-empty query string. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be @@ -2620,13 +2739,15 @@ declare module 'vscode' { } /** - * A workspace edit represents textual and files changes for + * A workspace edit is a collection of textual and files changes for * multiple resources and documents. + * + * Use the [applyEdit](#workspace.applyEdit)-function to apply a workspace edit. */ export class WorkspaceEdit { /** - * The number of affected resources. + * The number of affected resources of textual or resource changes. */ readonly size: number; @@ -2657,7 +2778,8 @@ declare module 'vscode' { delete(uri: Uri, range: Range): void; /** - * Check if this edit affects the given resource. + * Check if a text edit for a resource exists. + * * @param uri A resource identifier. * @return `true` if the given resource will be touched by this edit. */ @@ -2679,6 +2801,33 @@ declare module 'vscode' { */ get(uri: Uri): TextEdit[]; + /** + * Create a regular file. + * + * @param uri Uri of the new file.. + * @param options Defines if an existing file should be overwritten or be + * ignored. When overwrite and ignoreIfExists are both set overwrite wins. + */ + createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void; + + /** + * Delete a file or folder. + * + * @param uri The uri of the file that is to be deleted. + */ + deleteFile(uri: Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }): void; + + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten or be + * ignored. When overwrite and ignoreIfExists are both set overwrite wins. + */ + renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void; + + /** * Get all text edits grouped by resource. * @@ -2874,8 +3023,8 @@ declare module 'vscode' { export class ParameterInformation { /** - * The label of this signature. Will be shown in - * the UI. + * The label of this signature. *Note*: Must be a substring of its + * containing signature information's [label](#SignatureInformation.label). */ label: string; @@ -3054,6 +3203,13 @@ declare module 'vscode' { */ filterText?: string; + /** + * Select this item when showing. *Note* that only one completion item can be selected and + * that the editor decides which item that is. The rule is that the *first* item of those + * that match best is selected. + */ + preselect?: boolean; + /** * A string or snippet that should be inserted in a document when selecting * this completion. When `falsy` the [label](#CompletionItem.label) @@ -3428,6 +3584,7 @@ declare module 'vscode' { * [Region](#FoldingRangeKind.Region). The kind is used to categorize folding ranges and used by commands * like 'Fold all comments'. See * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of all kinds. + * If not set, the range is originated from a syntax element. */ kind?: FoldingRangeKind; @@ -3442,7 +3599,10 @@ declare module 'vscode' { } /** - * An enumeration of all folding range kinds. The kind is used to categorize folding ranges. + * An enumeration of specific folding range kinds. The kind is an optional field of a [FoldingRange](#FoldingRange) + * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like + * `Fold all comments` or `Fold all regions`. + * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers. */ export enum FoldingRangeKind { /** @@ -3454,7 +3614,7 @@ declare module 'vscode' { */ Imports = 2, /** - * Kind for folding range representing regions (for example a folding range marked by `#region` and `#endregion`). + * Kind for folding range representing regions originating from folding markers like `#region` and `#endregion`. */ Region = 3 } @@ -3884,6 +4044,23 @@ declare module 'vscode' { constructor(location: Location, message: string); } + /** + * Additional metadata about the type of a diagnostic. + */ + export enum DiagnosticTag { + /** + * Unused or unnecessary code. + * + * Diagnostics with this tag are rendered faded out. The amount of fading + * is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For + * example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the + * code with 75% opacity. For high contrast themes, use the + * `"editorUnnecessaryCode.border"` theme color to underline unnecessary code + * instead of fading it out. + */ + Unnecessary = 1, + } + /** * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects * are only valid in the scope of a file. @@ -3924,6 +4101,11 @@ declare module 'vscode' { */ relatedInformation?: DiagnosticRelatedInformation[]; + /** + * Additional metadata about the diagnostic. + */ + tags?: DiagnosticTag[]; + /** * Creates a new diagnostic object. * @@ -4020,17 +4202,23 @@ declare module 'vscode' { } /** - * Denotes a column in the editor window. Columns are - * used to show editors side by side. + * Denotes a location of an editor in the window. Editors can be arranged in a grid + * and each column represents one editor location in that grid by counting the editors + * in order of their appearance. */ export enum ViewColumn { /** - * A *symbolic* editor column representing the currently - * active column. This value can be used when opening editors, but the - * *resolved* [viewColumn](#TextEditor.viewColumn)-value of editors will always - * be `One`, `Two`, `Three`, or `undefined` but never `Active`. + * A *symbolic* editor column representing the currently active column. This value + * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. */ Active = -1, + /** + * A *symbolic* editor column representing the column to the side of the active one. This value + * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. + */ + Beside = -2, /** * The first editor column. */ @@ -4042,7 +4230,31 @@ declare module 'vscode' { /** * The third editor column. */ - Three = 3 + Three = 3, + /** + * The fourth editor column. + */ + Four = 4, + /** + * The fifth editor column. + */ + Five = 5, + /** + * The sixth editor column. + */ + Six = 6, + /** + * The seventh editor column. + */ + Seven = 7, + /** + * The eighth editor column. + */ + Eight = 8, + /** + * The ninth editor column. + */ + Nine = 9 } /** @@ -4329,6 +4541,13 @@ declare module 'vscode' { * [`globalState`](#ExtensionContext.globalState) to store key value data. */ storagePath: string | undefined; + + /** + * An absolute file path of a directory in which the extension can create log files. + * The directory might not exist on disk and creation is up to the extension. However, + * the parent directory is guaranteed to be existent. + */ + logPath: string; } /** @@ -4450,22 +4669,22 @@ declare module 'vscode' { /** * The clean task group; */ - public static Clean: TaskGroup; + static Clean: TaskGroup; /** * The build task group; */ - public static Build: TaskGroup; + static Build: TaskGroup; /** * The rebuild all task group; */ - public static Rebuild: TaskGroup; + static Rebuild: TaskGroup; /** * The test all task group; */ - public static Test: TaskGroup; + static Test: TaskGroup; private constructor(id: string, label: string); } @@ -5290,7 +5509,7 @@ declare module 'vscode' { /** * Content settings for the webview. */ - readonly options: WebviewOptions; + options: WebviewOptions; /** * Contents of the webview. @@ -5358,6 +5577,11 @@ declare module 'vscode' { */ title: string; + /** + * Icon for the panel shown in UI. + */ + iconPath?: Uri | { light: Uri; dark: Uri }; + /** * Webview belonging to the panel. */ @@ -5371,13 +5595,16 @@ declare module 'vscode' { /** * Editor position of the panel. This property is only set if the webview is in * one of the editor view columns. - * - * @deprecated */ readonly viewColumn?: ViewColumn; /** - * Is the panel currently visible? + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. */ readonly visible: boolean; @@ -5427,6 +5654,52 @@ declare module 'vscode' { readonly webviewPanel: WebviewPanel; } + /** + * Restore webview panels that have been persisted when vscode shuts down. + * + * There are two types of webview persistence: + * + * - Persistence within a session. + * - Persistence across sessions (across restarts of VS Code). + * + * A `WebviewPanelSerializer` is only required for the second case: persisting a webview across sessions. + * + * Persistence within a session allows a webview to save its state when it becomes hidden + * and restore its content from this state when it becomes visible again. It is powered entirely + * by the webview content itself. To save off a persisted state, call `acquireVsCodeApi().setState()` with + * any json serializable object. To restore the state again, call `getState()` + * + * ```js + * // Within the webview + * const vscode = acquireVsCodeApi(); + * + * // Get existing state + * const oldState = vscode.getState() || { value: 0 }; + * + * // Update state + * setState({ value: oldState.value + 1 }) + * ``` + * + * A `WebviewPanelSerializer` extends this persistence across restarts of VS Code. When the editor is shutdown, + * VS Code will save off the state from `setState` of all webviews that have a serializer. When the + * webview first becomes visible after the restart, this state is passed to `deserializeWebviewPanel`. + * The extension can then restore the old `WebviewPanel` from this state. + */ + interface WebviewPanelSerializer { + /** + * Restore a webview panel from its seriailzed `state`. + * + * Called when a serialized webview first becomes visible. + * + * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. The + * 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. + */ + deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; + } + /** * Namespace describing the environment the editor runs in. */ @@ -5574,6 +5847,21 @@ declare module 'vscode' { readonly focused: boolean; } + /** + * A uri handler is responsible for handling system-wide [uris](#Uri). + * + * @see [window.registerUriHandler](#window.registerUriHandler). + */ + export interface UriHandler { + + /** + * Handle the provided system-wide [uri](#Uri). + * + * @see [window.registerUriHandler](#window.registerUriHandler). + */ + handleUri(uri: Uri): ProviderResult; + } + /** * Namespace for dealing with the current window of the editor. That is visible * and active editors, as well as, UI elements to show messages, selections, and @@ -5612,7 +5900,7 @@ declare module 'vscode' { export const onDidChangeTextEditorSelection: Event; /** - * An [event](#Event) which fires when the selection in an editor has changed. + * An [event](#Event) which fires when the visible ranges of an editor has changed. */ export const onDidChangeTextEditorVisibleRanges: Event; @@ -5626,6 +5914,17 @@ declare module 'vscode' { */ export const onDidChangeTextEditorViewColumn: Event; + /** + * The currently opened terminals or an empty array. + */ + export const terminals: ReadonlyArray; + + /** + * An [event](#Event) which fires when a terminal has been created, either through the + * [createTerminal](#window.createTerminal) API or commands. + */ + export const onDidOpenTerminal: Event; + /** * An [event](#Event) which fires when a terminal is disposed. */ @@ -5650,8 +5949,8 @@ declare module 'vscode' { * * @param document A text document to be shown. * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [active](#ViewColumn.Active), other values - * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is - * not adjusted. + * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) + * to open the editor to the side of the currently active one. * @param preserveFocus When `true` the editor will not take focus. * @return A promise that resolves to an [editor](#TextEditor). */ @@ -5902,6 +6201,29 @@ declare module 'vscode' { */ export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; + /** + * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * of items of type T. + * + * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * + * @return A new [QuickPick](#QuickPick). + */ + export function createQuickPick(): QuickPick; + + /** + * Creates a [InputBox](#InputBox) to let the user enter some text input. + * + * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) + * is easier to use. [window.createInputBox](#window.createInputBox) should be used + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * + * @return A new [InputBox](#InputBox). + */ + export function createInputBox(): InputBox; + /** * Create a new [output channel](#OutputChannel) with the given name. * @@ -6033,6 +6355,42 @@ declare module 'vscode' { * @returns a [TreeView](#TreeView). */ export function createTreeView(viewId: string, options: { treeDataProvider: TreeDataProvider }): TreeView; + + /** + * Registers a [uri handler](#UriHandler) capable of handling system-wide [uris](#Uri). + * In case there are multiple windows open, the topmost window will handle the uri. + * A uri handler is scoped to the extension it is contributed from; it will only + * be able to handle uris which are directed to the extension itself. A uri must respect + * the following rules: + * + * - The uri-scheme must be the product name; + * - The uri-authority must be the extension id (eg. `my.extension`); + * - The uri-path, -query and -fragment parts are arbitrary. + * + * For example, if the `my.extension` extension registers a uri handler, it will only + * be allowed to handle uris with the prefix `product-name://my.extension`. + * + * An extension can only register a single uri handler in its entire activation lifetime. + * + * * *Note:* There is an activation event `onUri` that fires when a uri directed for + * the current extension is about to be handled. + * + * @param handler The uri handler to register for this extension. + */ + export function registerUriHandler(handler: UriHandler): Disposable; + + /** + * Registers a webview panel serializer. + * + * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation event and + * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. + * + * Only a single serializer may be registered at a time for a given `viewType`. + * + * @param viewType Type of the webview panel that can be serialized. + * @param serializer Webview serializer. + */ + export function registerWebviewPanelSerializer(viewType: string, serializer: WebviewPanelSerializer): Disposable; } /** @@ -6043,7 +6401,31 @@ declare module 'vscode' { /** * Element that is expanded or collapsed. */ - element: T; + readonly element: T; + + } + + /** + * The event that is fired when there is a change in [tree view's selection](#TreeView.selection) + */ + export interface TreeViewSelectionChangeEvent { + + /** + * Selected elements. + */ + readonly selection: T[]; + + } + + /** + * The event that is fired when there is a change in [tree view's visibility](#TreeView.visible) + */ + export interface TreeViewVisibilityChangeEvent { + + /** + * `true` if the [tree view](#TreeView) is visible otherwise `false`. + */ + readonly visible: boolean; } @@ -6065,19 +6447,36 @@ declare module 'vscode' { /** * Currently selected elements. */ - readonly selection: ReadonlyArray; + readonly selection: T[]; /** - * Reveal an element. By default revealed element is selected. + * Event that is fired when the [selection](#TreeView.selection) has changed + */ + readonly onDidChangeSelection: Event>; + + /** + * `true` if the [tree view](#TreeView) is visible otherwise `false`. + */ + readonly visible: boolean; + + /** + * Event that is fired when [visibility](TreeView.visible) has changed + */ + readonly onDidChangeVisibility: Event; + + /** + * Reveals the given element in the tree view. + * If the tree view is not visible then the tree view is shown and element is revealed. * + * By default revealed element is selected and not focused. * In order to not to select, set the option `select` to `false`. + * In order to focus, set the option `focus` to `true`. * * **NOTE:** [TreeDataProvider](#TreeDataProvider) is required to implement [getParent](#TreeDataProvider.getParent) method to access this API. */ - reveal(element: T, options?: { select?: boolean }): Thenable; + reveal(element: T, options?: { select?: boolean, focus?: boolean }): Thenable; } - /** * A data provider that provides tree data */ @@ -6151,7 +6550,7 @@ declare module 'vscode' { tooltip?: string | undefined; /** - * The [command](#Command) which should be run when the tree item is selected. + * The [command](#Command) that should be executed when the tree item is selected. */ command?: Command; @@ -6289,6 +6688,269 @@ declare module 'vscode' { cancellable?: boolean; } + /** + * A light-weight user input UI that is intially 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), + * the user pressing Esc, some other input UI opening, etc.) + * + * A user pressing Enter or some other gesture implying acceptance + * of the current state does not automatically hide this UI component. + * It is up to the extension to decide whether to accept the user's input + * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). + * + * When the extension no longer needs this input UI, it should + * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up + * any resources associated with it. + * + * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. + */ + export interface QuickInput { + + /** + * An optional title. + */ + title: string | undefined; + + /** + * An optional current step count. + */ + step: number | undefined; + + /** + * An optional total step count. + */ + totalSteps: number | undefined; + + /** + * If the UI should allow for user input. Defaults to true. + * + * Change this to false, e.g., while validating user input or + * loading data for the next step in user input. + */ + enabled: boolean; + + /** + * If the UI should show a progress indicator. Defaults to false. + * + * Change this to true, e.g., while loading more data or validating + * user input. + */ + busy: boolean; + + /** + * If the UI should stay open even when loosing UI focus. Defaults to false. + */ + ignoreFocusOut: boolean; + + /** + * Makes the input UI visible in its current configuration. Any other input + * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. + */ + show(): void; + + /** + * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) + * event. + */ + hide(): void; + + /** + * An event signaling when this input UI is hidden. + * + * 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), + * the user pressing Esc, some other input UI opening, etc.) + */ + onDidHide: Event; + + /** + * Dispose of this input UI and any associated resources. If it is still + * visible, it is first hidden. After this call the input UI is no longer + * functional and no additional methods or properties on it should be + * accessed. Instead a new input UI should be created. + */ + dispose(): void; + } + + /** + * A concrete [QuickInput](#QuickInput) to let the user pick an item from a + * list of items of type T. The items can be filtered through a filter text field and + * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for + * selecting multiple items. + * + * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + */ + export interface QuickPick extends QuickInput { + + /** + * Current value of the filter text. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * An event signaling when the value of the filter text has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the selected item(s). + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * Items to pick from. + */ + items: ReadonlyArray; + + /** + * If multiple items can be selected at the same time. Defaults to false. + */ + canSelectMany: boolean; + + /** + * If the filter text should also be matched against the description of the items. Defaults to false. + */ + matchOnDescription: boolean; + + /** + * If the filter text should also be matched against the detail of the items. Defaults to false. + */ + matchOnDetail: boolean; + + /** + * Active items. This can be read and updated by the extension. + */ + activeItems: ReadonlyArray; + + /** + * An event signaling when the active items have changed. + */ + readonly onDidChangeActive: Event; + + /** + * Selected items. This can be read and updated by the extension. + */ + selectedItems: ReadonlyArray; + + /** + * An event signaling when the selected items have changed. + */ + readonly onDidChangeSelection: Event; + } + + /** + * A concrete [QuickInput](#QuickInput) to let the user input a text value. + * + * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) + * is easier to use. [window.createInputBox](#window.createInputBox) should be used + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + */ + export interface InputBox extends QuickInput { + + /** + * Current input value. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * If the input value should be hidden. Defaults to false. + */ + password: boolean; + + /** + * An event signaling when the value has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the input value. + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * An optional prompt text providing some ask or explanation to the user. + */ + prompt: string | undefined; + + /** + * An optional validation message indicating a problem with the current input value. + */ + validationMessage: string | undefined; + } + + /** + * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). + */ + export interface QuickInputButton { + + /** + * Icon for the button. + */ + readonly iconPath: Uri | { light: Uri; dark: Uri } | ThemeIcon; + + /** + * An optional tooltip. + */ + readonly tooltip?: string | undefined; + } + + /** + * Predefined buttons for [QuickPick](#QuickPick) and [InputBox](#InputBox). + */ + export class QuickInputButtons { + + /** + * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * + * When a navigation 'back' button is needed this one should be used for consistency. + * It comes with a predefined icon, tooltip and location. + */ + static readonly Back: QuickInputButton; + + /** + * @hidden + */ + private constructor(); + } + /** * An event describing an individual change in the text of a [document](#TextDocument). */ @@ -6593,12 +7255,17 @@ declare module 'vscode' { export function saveAll(includeUntitled?: boolean): Thenable; /** - * Make changes to one or many resources as defined by the given + * Make changes to one or many resources or create, delete, and rename resources as defined by the given * [workspace edit](#WorkspaceEdit). * - * When applying a workspace edit, the editor implements an 'all-or-nothing'-strategy, - * that means failure to load one document or make changes to one document will cause - * the edit to be rejected. + * All changes of a workspace edit are applied in the same order in which they have been added. If + * multiple textual inserts are made at the same position, these strings appear in the resulting text + * in the order the 'inserts' were made. Invalid sequences like 'delete file a' -> 'insert text in file a' + * 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 + * not be attempted, when a single edit fails. * * @param edit A workspace edit. * @return A thenable that resolves when the edit could be applied. @@ -6731,13 +7398,13 @@ declare module 'vscode' { export const onDidChangeConfiguration: Event; /** - * Register a task provider. + * ~~Register a task provider.~~ + * + * @deprecated Use the corresponding function on the `tasks` namespace instead * * @param type The task kind type this provider is registered for. * @param provider A task provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - * - * @deprecated Use the corresponding function on the `tasks` namespace instead */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; @@ -6752,7 +7419,7 @@ declare module 'vscode' { * @param options Immutable metadata about the provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { isCaseSensitive?: boolean }): Disposable; + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean }): Disposable; } /** @@ -7659,7 +8326,7 @@ declare module 'vscode' { /** * Namespace for dealing with installed extensions. Extensions are represented - * by an [extension](#Extension)-interface which allows to reflect on them. + * by an [extension](#Extension)-interface which enables reflection on them. * * Extension writers can provide APIs to other extensions by returning their API public * surface from the `activate`-call. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7f063470703..c1ac3219150 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// This is the place for API experiments and proposal. +// This is the place for API experiments and proposals. declare module 'vscode' { @@ -11,46 +11,360 @@ declare module 'vscode' { export function sampleFunction(): Thenable; } - //#region Joh: remote, search provider + export namespace languages { + /** + * + */ + export function changeLanguage(document: TextDocument, languageId: string): Thenable; + } + + //#region Joh - read/write in chunks + + export interface FileSystemProvider { + open?(resource: Uri): 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; + } + + //#endregion + + //#region Rob: search provider + + /** + * The parameters of a query for text search. + */ export interface TextSearchQuery { + /** + * The text pattern to search for. + */ pattern: string; - isRegExp: boolean; - isCaseSensitive: boolean; - isWordMatch: boolean; + + /** + * Whether or not `pattern` should be interpreted as a regular expression. + */ + isRegExp?: boolean; + + /** + * Whether or not the search should be case-sensitive. + */ + isCaseSensitive?: boolean; + + /** + * Whether or not to search for whole word matches only. + */ + isWordMatch?: boolean; } + /** + * A file glob pattern to match file paths against. + * TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts. + * @see [GlobPattern](#GlobPattern) + */ + export type GlobString = string; + + /** + * Options common to file and text search + */ export interface SearchOptions { + /** + * The root folder to search within. + */ folder: Uri; - includes: string[]; // paths relative to folder - excludes: string[]; - useIgnoreFiles?: boolean; - followSymlinks?: boolean; + + /** + * Files that match an `includes` glob pattern should be included in the search. + */ + includes: GlobString[]; + + /** + * Files that match an `excludes` glob pattern should be excluded from the search. + */ + excludes: GlobString[]; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks: boolean; } + /** + * Options to specify the size of the result text preview. + * These options don't affect the size of the match itself, just the amount of preview text. + */ + export interface TextSearchPreviewOptions { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + */ + maxLines: number; + + /** + * The maximum number of characters included before the start of the match. + */ + leadingChars: number; + + /** + * The maximum number of characters included per line. + */ + totalChars: number; + } + + /** + * Options that apply to text search. + */ export interface TextSearchOptions extends SearchOptions { - previewOptions?: any; // total length? # of context lines? leading and trailing # of chars? + /** + * The maximum number of results to be returned. + */ + maxResults: number; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ maxFileSize?: number; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ encoding?: string; } - export interface FileSearchOptions extends SearchOptions { } - - export interface TextSearchResult { - path: string; - range: Range; - - // For now, preview must be a single line of text - preview: { text: string, match: Range }; + /** + * The parameters of a query for file search. + */ + export interface FileSearchQuery { + /** + * The search pattern to match against file paths. + */ + pattern: string; } - export interface SearchProvider { - provideFileSearchResults?(options: FileSearchOptions, progress: Progress, token: CancellationToken): Thenable; - provideTextSearchResults?(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; + /** + * Options that apply to file search. + */ + export interface FileSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults: number; + } + + /** + * Options that apply to requesting the file index. + */ + export interface FileIndexOptions extends SearchOptions { } + + /** + * A preview of the text result. + */ + export interface TextSearchResultPreview { + /** + * The matching line of text, or a portion of the matching line that contains the match. + * For now, this can only be a single line. + */ + text: string; + + /** + * The Range within `text` corresponding to the text of the match. + */ + match: Range; + } + + /** + * A match from a text search + */ + export interface TextSearchResult { + /** + * The uri for the matching document. + */ + uri: Uri; + + /** + * The range of the match within the document. + */ + range: Range; + + /** + * A preview of the text result. + */ + preview: TextSearchResultPreview; + } + + /** + * A FileIndexProvider provides a list of files in the given folder. VS Code will filter that list for searching with quickopen or from other extensions. + * + * A FileIndexProvider is the simpler of two ways to implement file search in VS Code. Use a FileIndexProvider if you are able to provide a listing of all files + * in a folder, and want VS Code to filter them according to the user's search query. + * + * The FileIndexProvider will be invoked once when quickopen is opened, and VS Code will filter the returned list. It will also be invoked when + * `workspace.findFiles` is called. + * + * If a [`FileSearchProvider`](#FileSearchProvider) is registered for the scheme, that provider will be used instead. + */ + export interface FileIndexProvider { + /** + * Provide the set of files in the folder. + * @param options A set of options to consider while searching. + * @param token A cancellation token. + */ + provideFileIndex(options: FileIndexOptions, token: CancellationToken): Thenable; + } + + /** + * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions. + * + * A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for + * all files that match the user's query. + * + * The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string, + * and in that case, every file in the folder should be returned. + * + * @see [FileIndexProvider](#FileIndexProvider) + */ + export interface FileSearchProvider { + /** + * Provide the set of files that match a certain file path pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching files. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): Thenable; + } + + /** + * A TextSearchProvider provides search results for text results inside files in the workspace. + */ + export interface TextSearchProvider { + /** + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; + } + + /** + * Options that can be set on a findTextInFiles search. + */ + export interface FindTextInFilesOptions { + /** + * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + */ + include?: GlobPattern; + + /** + * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. + */ + exclude?: GlobPattern | null; + + /** + * The maximum number of results to search for + */ + maxResults?: number; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles?: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks?: boolean; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; } export namespace workspace { - export function registerSearchProvider(scheme: string, provider: SearchProvider): Disposable; + /** + * DEPRECATED + */ + export function registerSearchProvider(): Disposable; + + /** + * Register a file index provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerFileIndexProvider(scheme: string, provider: FileIndexProvider): Disposable; + + /** + * Register a search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; + + /** + * Register a text search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ + export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; + + /** + * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. + * @param options An optional set of query options. Include and exclude patterns, maxResults, etc. + * @param callback A callback, called for each result + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves when the search is complete. + */ + export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } //#endregion @@ -92,12 +406,12 @@ declare module 'vscode' { //todo@joh -> make class export interface DecorationData { - priority?: number; + letter?: string; title?: string; - bubble?: boolean; - abbreviation?: string; color?: ThemeColor; - source?: string; + priority?: number; + bubble?: boolean; + source?: string; // hacky... we should remove it and use equality under the hood } export interface SourceControlResourceDecorations { @@ -169,39 +483,16 @@ declare module 'vscode' { Off = 7 } - /** - * A logger for writing to an extension's log file, and accessing its dedicated log directory. - */ - export interface Logger { - trace(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - info(message: string, ...args: any[]): void; - warn(message: string, ...args: any[]): void; - error(message: string | Error, ...args: any[]): void; - critical(message: string | Error, ...args: any[]): void; - } - - export interface ExtensionContext { - /** - * This extension's logger - */ - logger: Logger; - - /** - * Path where an extension can write log files. - * - * Extensions must create this directory before writing to it. The parent directory will always exist. - */ - readonly logDirectory: string; - } - export namespace env { /** * Current logging level. - * - * @readonly */ export const logLevel: LogLevel; + + /** + * An [event](#Event) that fires when the log level has changed. + */ + export const onDidChangeLogLevel: Event; } //#endregion @@ -256,6 +547,23 @@ declare module 'vscode' { //#endregion + //#region Joao: SCM selected provider + + export interface SourceControl { + + /** + * Whether the source control is selected. + */ + readonly selected: boolean; + + /** + * An event signaling when the selection state changes. + */ + readonly onDidChangeSelection: Event; + } + + //#endregion + //#region Comments /** * Comments provider related APIs are still in early stages, they may be changed significantly during our API experiments. @@ -312,17 +620,14 @@ declare module 'vscode' { interface DocumentCommentProvider { provideDocumentComments(document: TextDocument, token: CancellationToken): Promise; - createNewCommentThread?(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; - replyToCommentThread?(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; - onDidChangeCommentThreads?: Event; + createNewCommentThread(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; + onDidChangeCommentThreads: Event; } interface WorkspaceCommentProvider { provideWorkspaceComments(token: CancellationToken): Promise; - createNewCommentThread?(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; - replyToCommentThread?(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; - - onDidChangeCommentThreads?: Event; + onDidChangeCommentThreads: Event; } namespace workspace { @@ -337,53 +642,143 @@ declare module 'vscode' { /** * Fires when the terminal's pty slave pseudo-device is written to. In other words, this * provides access to the raw data stream from the process running within the terminal, - * including ANSI sequences. + * including VT sequences. */ - onData: Event; + onDidWriteData: Event; } - export namespace window { + /** + * Represents the dimensions of a terminal. + */ + export interface TerminalDimensions { /** - * The currently opened terminals or an empty array. + * The number of columns in the terminal. + */ + readonly columns: number; + + /** + * The number of rows in the terminal. + */ + readonly rows: number; + } + + /** + * Represents a terminal without a process where all interaction and output in the terminal is + * controlled by an extension. This is similar to an output window but has the same VT sequence + * compatility as the regular terminal. + * + * Note that an instance of [Terminal](#Terminal) will be created when a TerminalRenderer is + * created with all its APIs available for use by extensions. When using the Terminal object + * of a TerminalRenderer it acts just like normal only the extension that created the + * TerminalRenderer essentially acts as a process. For example when an + * [Terminal.onDidWriteData](#Terminal.onDidWriteData) listener is registered, that will fire + * when [TerminalRenderer.write](#TerminalRenderer.write) is called. Similarly when + * [Terminal.sendText](#Terminal.sendText) is triggered that will fire the + * [TerminalRenderer.onDidAcceptInput](#TerminalRenderer.onDidAcceptInput) event. + * + * **Example:** Create a terminal renderer, show it and write hello world in red + * ```typescript + * const renderer = window.createTerminalRenderer('foo'); + * renderer.terminal.then(t => t.show()); + * renderer.write('\x1b[31mHello world\x1b[0m'); + * ``` + */ + export interface TerminalRenderer { + /** + * The name of the terminal, this will appear in the terminal selector. + */ + name: string; + + /** + * The dimensions of the terminal, the rows and columns of the terminal can only be set to + * a value smaller than the maximum value, if this is undefined the terminal will auto fit + * to the maximum value [maximumDimensions](TerminalRenderer.maximumDimensions). * - * @readonly + * **Example:** Override the dimensions of a TerminalRenderer to 20 columns and 10 rows + * ```typescript + * terminalRenderer.dimensions = { + * cols: 20, + * rows: 10 + * }; + * ``` */ - export let terminals: Terminal[]; + dimensions: TerminalDimensions | undefined; /** - * An [event](#Event) which fires when a terminal has been created, either through the - * [createTerminal](#window.createTerminal) API or commands. + * The maximum dimensions of the terminal, this will be undefined immediately after a + * terminal renderer is created and also until the terminal becomes visible in the UI. + * Listen to [onDidChangeMaximumDimensions](TerminalRenderer.onDidChangeMaximumDimensions) + * to get notified when this value changes. */ - export const onDidOpenTerminal: Event; - } + readonly maximumDimensions: TerminalDimensions | undefined; - //#endregion + /** + * The corressponding [Terminal](#Terminal) for this TerminalRenderer. + */ + readonly terminal: Terminal; - //#region URLs + /** + * Write text to the terminal. Unlike [Terminal.sendText](#Terminal.sendText) which sends + * text to the underlying _process_, this will write the text to the terminal itself. + * + * **Example:** Write red text to the terminal + * ```typescript + * terminalRenderer.write('\x1b[31mHello world\x1b[0m'); + * ``` + * + * **Example:** Move the cursor to the 10th row and 20th column and write an asterisk + * ```typescript + * terminalRenderer.write('\x1b[10;20H*'); + * ``` + * + * @param text The text to write. + */ + write(text: string): void; - export interface ProtocolHandler { - handleUri(uri: Uri): void; + /** + * An event which fires on keystrokes in the terminal or when an extension calls + * [Terminal.sendText](#Terminal.sendText). Keystrokes are converted into their + * corresponding VT sequence representation. + * + * **Example:** Simulate interaction with the terminal from an outside extension or a + * workbench command such as `workbench.action.terminal.runSelectedText` + * ```typescript + * const terminalRenderer = window.createTerminalRenderer('test'); + * terminalRenderer.onDidAcceptInput(data => { + * cosole.log(data); // 'Hello world' + * }); + * terminalRenderer.terminal.then(t => t.sendText('Hello world')); + * ``` + */ + readonly onDidAcceptInput: Event; + + /** + * An event which fires when the [maximum dimensions](#TerminalRenderer.maimumDimensions) of + * the terminal renderer change. + */ + readonly onDidChangeMaximumDimensions: Event; } export namespace window { + /** + * The currently active terminal or `undefined`. The active terminal is the one that + * currently has focus or most recently had focus. + */ + export const activeTerminal: Terminal | undefined; /** - * Registers a protocol handler capable of handling system-wide URIs. + * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) + * has changed. *Note* that the event also fires when the active terminal changes + * to `undefined`. */ - export function registerProtocolHandler(handler: ProtocolHandler): Disposable; - } + export const onDidChangeActiveTerminal: Event; - //#endregion - - //#region Joh: hierarchical document symbols, https://github.com/Microsoft/vscode/issues/34968 - - export class SymbolInformation2 extends SymbolInformation { - definingRange: Range; - children: SymbolInformation2[]; - } - - export interface DocumentSymbolProvider { - provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult; + /** + * Create a [TerminalRenderer](#TerminalRenderer). + * + * @param name The name of the terminal renderer, this shows up in the terminal selector. + */ + export function createTerminalRenderer(name: string): TerminalRenderer; } //#endregion @@ -396,96 +791,68 @@ declare module 'vscode' { //#endregion - //#region mjbvz: Unused diagnostics - /** - * Additional metadata about the type of diagnostic. - */ - export enum DiagnosticTag { - /** - * Unused or unnecessary code. - */ - Unnecessary = 1, + //#region mjbvz,joh: https://github.com/Microsoft/vscode/issues/43768 + export interface FileRenameEvent { + readonly oldUri: Uri; + readonly newUri: Uri; } - export interface Diagnostic { - /** - * Additional metadata about the type of the diagnostic. - */ - customTags?: DiagnosticTag[]; - } - - //#endregion - - //#region mjbvz: File rename events - export interface ResourceRenamedEvent { - readonly oldResource: Uri; - readonly newResource: Uri; + export interface FileWillRenameEvent { + readonly oldUri: Uri; + readonly newUri: Uri; + waitUntil(thenable: Thenable): void; } export namespace workspace { - export const onDidRenameResource: Event; + export const onWillRenameFile: Event; + export const onDidRenameFile: Event; } //#endregion - //#region mjbvz: Code action trigger + //#region Signature Help + /** + * How a [Signature provider](#SignatureHelpProvider) was triggered + */ + export enum SignatureHelpTriggerReason { + /** + * Signature help was invoked manually by the user or by a command. + */ + Invoke = 1, + + /** + * Signature help was triggered by a trigger character. + */ + TriggerCharacter = 2, + + /** + * Signature help was retriggered. + * + * Retriggers occur when the signature help is already active and can be caused by typing a trigger character + * or by a cursor move. + */ + Retrigger = 3, + } /** - * How a [code action provider](#CodeActionProvider) was triggered + * Contains additional information about the context in which a + * [signature help provider](#SignatureHelpProvider.provideSignatureHelp) is triggered. */ - export enum CodeActionTrigger { + export interface SignatureHelpContext { /** - * Provider was triggered automatically by VS Code. + * Action that caused signature help to be requested. */ - Automatic = 1, + readonly triggerReason: SignatureHelpTriggerReason; /** - * User requested code actions. + * Character that caused signature help to be requested. + * + * This is `undefined` for manual triggers or retriggers for a cursor move. */ - Manual = 2, + readonly triggerCharacter?: string; } - interface CodeActionContext { - /** - * How the code action provider was triggered. - */ - triggerKind?: CodeActionTrigger; - } - - //#endregion - - - //#region Matt: WebView Serializer - - /** - * Restore webview panels that have been persisted when vscode shuts down. - */ - interface WebviewPanelSerializer { - /** - * Restore a webview panel from its seriailzed `state`. - * - * Called when a serialized webview first becomes visible. - * - * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. - * @param state Persisted state. - * - * @return Thanble indicating that the webview has been fully restored. - */ - deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; - } - - namespace window { - /** - * Registers a webview panel serializer. - * - * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation method and - * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. - * - * Only a single serializer may be registered at a time for a given `viewType`. - * - * @param viewType Type of the webview panel that can be serialized. - * @param serializer Webview serializer. - */ - export function registerWebviewPanelSerializer(viewType: string, serializer: WebviewPanelSerializer): Disposable; + export interface SignatureHelpProvider { + provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; } //#endregion diff --git a/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts b/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts index 45a415522da..7b079429f2c 100644 --- a/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsContainersExtensionPoint.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { join } from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import { createCSSRule } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; @@ -29,6 +29,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { URI } from 'vs/base/common/uri'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -36,6 +37,12 @@ export interface IUserFriendlyViewsContainerDescriptor { icon: string; } +export interface IUserFriendlyViewsContainerDescriptor2 { + id: string; + title: string; + icon: URI; +} + const viewsContainerSchema: IJSONSchema = { type: 'object', properties: { @@ -81,12 +88,13 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { private registerTestViewContainer(): void { const title = localize('test', "Test"); const cssClass = `extensionViewlet-test`; - const icon = require.toUrl('./media/test.svg'); + const icon = URI.parse(require.toUrl('./media/test.svg')); - this.registerCustomViewlet({ id: TEST_VIEW_CONTAINER_ID, title, icon }, TEST_VIEW_CONTAINER_ORDER, cssClass); + this.registerCustomViewlet({ id: TEST_VIEW_CONTAINER_ID, title, icon }, TEST_VIEW_CONTAINER_ORDER, cssClass, void 0); } private handleAndRegisterCustomViewContainers() { + let order = TEST_VIEW_CONTAINER_ORDER + 1; viewsContainersExtensionPoint.setHandler((extensions) => { for (let extension of extensions) { const { value, collector } = extension; @@ -96,7 +104,7 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { } switch (entry.key) { case 'activitybar': - this.registerCustomViewContainers(entry.value, extension.description); + order = this.registerCustomViewContainers(entry.value, extension.description, order); break; } }); @@ -132,23 +140,23 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { return true; } - private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription) { - containers.forEach((descriptor, index) => { + private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number): number { + containers.forEach(descriptor => { const cssClass = `extensionViewlet-${descriptor.id}`; - // TODO@extensionLocation - const icon = join(extension.extensionLocation.fsPath, descriptor.icon); - this.registerCustomViewlet({ id: `workbench.view.extension.${descriptor.id}`, title: descriptor.title, icon }, TEST_VIEW_CONTAINER_ORDER + index + 1, cssClass); + const icon = resources.joinPath(extension.extensionLocation, descriptor.icon); + this.registerCustomViewlet({ id: `workbench.view.extension.${descriptor.id}`, title: descriptor.title, icon }, order++, cssClass, extension.id); }); + return order; } - private registerCustomViewlet(descriptor: IUserFriendlyViewsContainerDescriptor, order: number, cssClass: string): void { + private registerCustomViewlet(descriptor: IUserFriendlyViewsContainerDescriptor2, order: number, cssClass: string, extensionId: string): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const viewletRegistry = Registry.as(ViewletExtensions.Viewlets); const id = descriptor.id; if (!viewletRegistry.getViewlet(id)) { - viewContainersRegistry.registerViewContainer(id); + viewContainersRegistry.registerViewContainer(id, extensionId); // Register as viewlet class CustomViewlet extends ViewContainerViewlet { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index d70fc465c8e..edc30f694e0 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -17,7 +17,6 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ProgressLocation } from 'vs/platform/progress/common/progress'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/parts/files/common/files'; import { VIEWLET_ID as SCM } from 'vs/workbench/parts/scm/common/scm'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/parts/debug/common/debug'; @@ -114,7 +113,7 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { } const registeredViews = ViewsRegistry.getViews(container); const viewIds = []; - const viewDescriptors = coalesce(entry.value.map(item => { + const viewDescriptors = coalesce(entry.value.map((item, index) => { // validate if (viewIds.indexOf(item.id) !== -1) { collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}` in the view container `{1}`", item.id, container.id)); @@ -133,7 +132,8 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, collapsed: this.showCollapsed(container), - treeViewer: this.instantiationService.createInstance(CustomTreeViewer, item.id, this.getProgressLocation(container)) + treeViewer: this.instantiationService.createInstance(CustomTreeViewer, item.id, container), + order: extension.description.id === container.extensionId ? index + 1 : void 0 }; viewIds.push(viewDescriptor.id); @@ -145,18 +145,6 @@ class ViewsContainersExtensionHandler implements IWorkbenchContribution { }); } - private getProgressLocation(container: ViewContainer): ProgressLocation { - switch (container.id) { - case EXPLORER: - return ProgressLocation.Explorer; - case SCM: - return ProgressLocation.Scm; - case DEBUG: - return null /* No debug progress location yet */; - } - return null; - } - private isValidViewDescriptors(viewDescriptors: IUserFriendlyViewDescriptor[], collector: ExtensionMessageCollector): boolean { if (!Array.isArray(viewDescriptors)) { collector.error(localize('requirearray', "views must be an array")); diff --git a/src/vs/workbench/api/electron-browser/mainThreadCommands.ts b/src/vs/workbench/api/electron-browser/mainThreadCommands.ts index ad4bfc1165a..3f6db29fc94 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadCommands.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadCommands.ts @@ -94,11 +94,11 @@ function _generateMarkdown(description: string | ICommandHandlerDescription): st parts.push('\n\n'); if (description.args) { for (let arg of description.args) { - parts.push(`* _${arg.name}_ ${arg.description || ''}\n`); + parts.push(`* _${arg.name}_ - ${arg.description || ''}\n`); } } if (description.returns) { - parts.push(`* _(returns)_ ${description.returns}`); + parts.push(`* _(returns)_ - ${description.returns}`); } parts.push('\n\n'); return parts.join(''); diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index fd3d2ef1e23..f1c5434edd9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -5,18 +5,18 @@ 'use strict'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as modes from 'vs/editor/common/modes'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import { keys } from '../../../base/common/map'; +import { keys } from 'vs/base/common/map'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape } from '../node/extHost.protocol'; import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { COMMENTS_PANEL_ID } from 'vs/workbench/parts/comments/electron-browser/commentsPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ReviewController } from 'vs/workbench/parts/comments/electron-browser/commentsEditorContribution'; @extHostNamedCustomer(MainContext.MainThreadComments) @@ -25,6 +25,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _proxy: ExtHostCommentsShape; private _documentProviders = new Map(); private _workspaceProviders = new Map(); + private _firstSessionStart: boolean; constructor( extHostContext: IExtHostContext, @@ -35,25 +36,28 @@ export class MainThreadComments extends Disposable implements MainThreadComments ) { super(); this._disposables = []; + this._firstSessionStart = true; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); this._disposables.push(this._editorService.onDidActiveEditorChange(e => { - const outerEditor = this.getFocusedEditor(); - if (!outerEditor) { + const editors = this.getFocusedEditors(); + if (!editors || !editors.length) { return; } - const controller = ReviewController.get(outerEditor); - if (!controller) { - return; - } + editors.forEach(editor => { + const controller = ReviewController.get(editor); + if (!controller) { + return; + } - if (!outerEditor.getModel()) { - return; - } + if (!editor.getModel()) { + return; + } - const outerEditorURI = outerEditor.getModel().uri; - this.provideDocumentComments(outerEditorURI).then(commentInfos => { - this._commentService.setComments(outerEditorURI, commentInfos.filter(info => info !== null)); + const outerEditorURI = editor.getModel().uri; + this.provideDocumentComments(outerEditorURI).then(commentInfos => { + this._commentService.setDocumentComments(outerEditorURI, commentInfos.filter(info => info !== null)); + }); }); })); } @@ -81,10 +85,13 @@ export class MainThreadComments extends Disposable implements MainThreadComments $registerWorkspaceCommentProvider(handle: number): void { this._workspaceProviders.set(handle, undefined); this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, true); - this._panelService.openPanel(COMMENTS_PANEL_ID); + if (this._firstSessionStart) { + this._panelService.openPanel(COMMENTS_PANEL_ID); + this._firstSessionStart = false; + } this._proxy.$provideWorkspaceComments(handle).then(commentThreads => { if (commentThreads) { - this._commentService.setAllComments(commentThreads); + this._commentService.setWorkspaceComments(handle, commentThreads); } }); } @@ -96,8 +103,10 @@ export class MainThreadComments extends Disposable implements MainThreadComments $unregisterWorkspaceCommentProvider(handle: number): void { this._workspaceProviders.delete(handle); - this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, false); - this._commentService.removeAllComments(); + if (this._workspaceProviders.size === 0) { + this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, false); + } + this._commentService.removeWorkspaceComments(handle); } $onDidCommentThreadsChange(handle: number, event: modes.CommentThreadChangedEvent) { @@ -113,13 +122,25 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._documentProviders.clear(); } - getFocusedEditor(): ICodeEditor { - let editor = this._codeEditorService.getFocusedCodeEditor(); - if (!editor) { - return this._editorService.activeTextEditorWidget as ICodeEditor; + getFocusedEditors(): ICodeEditor[] { + let activeControl = this._editorService.activeControl; + if (activeControl) { + if (isCodeEditor(activeControl.getControl())) { + return [this._editorService.activeControl.getControl() as ICodeEditor]; + } + + if (isDiffEditor(activeControl.getControl())) { + let diffEditor = activeControl.getControl() as IDiffEditor; + return [diffEditor.getOriginalEditor(), diffEditor.getModifiedEditor()]; + } } - return editor; + let editor = this._codeEditorService.getFocusedCodeEditor(); + + if (editor) { + return [editor]; + } + return []; } async provideWorkspaceComments(): Promise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadConfiguration.ts b/src/vs/workbench/api/electron-browser/mainThreadConfiguration.ts index 2b307d2d019..c375aa9faa7 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadConfiguration.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -36,12 +36,12 @@ export class MainThreadConfiguration implements MainThreadConfigurationShape { this._configurationListener.dispose(); } - $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any, resourceUriComponenets: UriComponents): TPromise { + $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any, resourceUriComponenets: UriComponents): Thenable { const resource = resourceUriComponenets ? URI.revive(resourceUriComponenets) : null; return this.writeConfiguration(target, key, value, resource); } - $removeConfigurationOption(target: ConfigurationTarget, key: string, resourceUriComponenets: UriComponents): TPromise { + $removeConfigurationOption(target: ConfigurationTarget, key: string, resourceUriComponenets: UriComponents): Thenable { const resource = resourceUriComponenets ? URI.revive(resourceUriComponenets) : null; return this.writeConfiguration(target, key, undefined, resource); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index c3e41323219..2d741ad475b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -5,8 +5,8 @@ 'use strict'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import uri from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable, ITerminalSettings, IDebugAdapter, IDebugAdapterProvider, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; +import { URI as uri } from 'vs/base/common/uri'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable, ITerminalSettings, IDebugAdapter, IDebugAdapterProvider } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, @@ -18,8 +18,6 @@ import { AbstractDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter import * as paths from 'vs/base/common/paths'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/common/debugUtils'; -import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -import { AbstractTerminalLauncher } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; @extHostNamedCustomer(MainContext.MainThreadDebugService) @@ -30,18 +28,23 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private _breakpointEventsActive: boolean; private _debugAdapters: Map; private _debugAdaptersHandleCounter = 1; - private _terminalLauncher: ITerminalLauncher; - constructor( extHostContext: IExtHostContext, - @IDebugService private debugService: IDebugService, - @ITerminalService private terminalService: ITerminalService, + @IDebugService private debugService: IDebugService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService); this._toDispose = []; - this._toDispose.push(debugService.onDidNewSession(proc => this._proxy.$acceptDebugSessionStarted(proc.getId(), proc.configuration.type, proc.getName(false)))); - this._toDispose.push(debugService.onDidEndSession(proc => this._proxy.$acceptDebugSessionTerminated(proc.getId(), proc.configuration.type, proc.getName(false)))); + this._toDispose.push(debugService.onDidNewSession(session => { + this._proxy.$acceptDebugSessionStarted(session.getId(), session.configuration.type, session.getName(false)); + })); + // Need to start listening early to new session events because a custom event can come while a session is initialising + this._toDispose.push(debugService.onWillNewSession(session => { + this._toDispose.push(session.onDidCustomEvent(event => this._proxy.$acceptDebugSessionCustomEvent(session.getId(), session.configuration.type, session.configuration.name, event))); + })); + this._toDispose.push(debugService.onDidEndSession(session => { + this._proxy.$acceptDebugSessionTerminated(session.getId(), session.configuration.type, session.getName(false)); + })); this._toDispose.push(debugService.getViewModel().onDidFocusSession(proc => { if (proc) { this._proxy.$acceptDebugSessionActiveChanged(proc.getId(), proc.configuration.type, proc.getName(false)); @@ -50,14 +53,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } })); - this._toDispose.push(debugService.onDidCustomEvent(event => { - if (event && event.sessionId) { - const process = this.debugService.getModel().getSessions().filter(p => p.getId() === event.sessionId).pop(); - if (process) { - this._proxy.$acceptDebugSessionCustomEvent(event.sessionId, process.configuration.type, process.configuration.name, event); - } - } - })); this._debugAdapters = new Map(); } @@ -73,21 +68,18 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { - return this._proxy.$substituteVariables(folder.uri, config); + return TPromise.wrap(this._proxy.$substituteVariables(folder ? folder.uri : undefined, config)); } runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - if (!this._terminalLauncher) { - this._terminalLauncher = new ExtensionTerminalLauncher(this.terminalService, this._proxy); - } - return this._terminalLauncher.runInTerminal(args, config); + return TPromise.wrap(this._proxy.$runInTerminal(args, config)); } public dispose(): void { this._toDispose = dispose(this._toDispose); } - public $startBreakpointEvents(): TPromise { + public $startBreakpointEvents(): Thenable { if (!this._breakpointEventsActive) { this._breakpointEventsActive = true; @@ -126,7 +118,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return TPromise.wrap(undefined); } - public $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise { + public $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): Thenable { for (let dto of DTOs) { if (dto.type === 'sourceMulti') { @@ -149,7 +141,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return void 0; } - public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): TPromise { + public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): Thenable { breakpointIds.forEach(id => this.debugService.removeBreakpoints(id)); functionBreakpointIds.forEach(id => this.debugService.removeFunctionBreakpoints(id)); return void 0; @@ -185,24 +177,24 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb }); } - public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasDebugAdapterExecutable: boolean, handle: number): TPromise { + public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasDebugAdapterExecutable: boolean, handle: number): Thenable { const provider = { type: debugType }; if (hasProvide) { provider.provideDebugConfigurations = folder => { - return this._proxy.$provideDebugConfigurations(handle, folder); + return TPromise.wrap(this._proxy.$provideDebugConfigurations(handle, folder)); }; } if (hasResolve) { provider.resolveDebugConfiguration = (folder, debugConfiguration) => { - return this._proxy.$resolveDebugConfiguration(handle, folder, debugConfiguration); + return TPromise.wrap(this._proxy.$resolveDebugConfiguration(handle, folder, debugConfiguration)); }; } if (hasDebugAdapterExecutable) { provider.debugAdapterExecutable = (folder) => { - return this._proxy.$debugAdapterExecutable(handle, folder); + return TPromise.wrap(this._proxy.$debugAdapterExecutable(handle, folder)); }; } this.debugService.getConfigurationManager().registerDebugConfigurationProvider(handle, provider); @@ -210,12 +202,12 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return TPromise.wrap(undefined); } - public $unregisterDebugConfigurationProvider(handle: number): TPromise { + public $unregisterDebugConfigurationProvider(handle: number): Thenable { this.debugService.getConfigurationManager().unregisterDebugConfigurationProvider(handle); return TPromise.wrap(undefined); } - public $startDebugging(_folderUri: uri | undefined, nameOrConfiguration: string | IConfig): TPromise { + public $startDebugging(_folderUri: uri | undefined, nameOrConfiguration: string | IConfig): Thenable { const folderUri = _folderUri ? uri.revive(_folderUri) : undefined; const launch = this.debugService.getConfigurationManager().getLaunch(folderUri); return this.debugService.startDebugging(launch, nameOrConfiguration).then(x => { @@ -225,10 +217,10 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb }); } - public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): TPromise { - const process = this.debugService.getModel().getSessions().filter(p => p.getId() === sessionId).pop(); - if (process) { - return process.raw.custom(request, args).then(response => { + public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Thenable { + const session = this.debugService.getSession(sessionId); + if (session) { + return session.customRequest(request, args).then(response => { if (response && response.success) { return response.body; } else { @@ -239,7 +231,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return TPromise.wrapError(new Error('debug session not found')); } - public $appendDebugConsole(value: string): TPromise { + public $appendDebugConsole(value: string): Thenable { // Use warning as severity to get the orange color for messages coming from the debug extension this.debugService.logToRepl(value, severity.Warning); return TPromise.wrap(undefined); @@ -283,7 +275,7 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter { } public startSession(): TPromise { - return this._proxy.$startDASession(this._handle, this._debugType, this._adapterExecutable, this._debugPort); + return TPromise.wrap(this._proxy.$startDASession(this._handle, this._debugType, this._adapterExecutable, this._debugPort)); } public sendMessage(message: DebugProtocol.ProtocolMessage): void { @@ -300,28 +292,6 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter { } public stopSession(): TPromise { - return this._proxy.$stopDASession(this._handle); - } -} - -export class ExtensionTerminalLauncher extends AbstractTerminalLauncher { - - constructor( - @ITerminalService terminalService: ITerminalService, - private _proxy: ExtHostDebugServiceShape - ) { - super(terminalService); - } - - protected runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return this._proxy.$runInTerminal(args, config); - } - - protected isBusy(processId: number): TPromise { - return this._proxy.$isTerminalBusy(processId); - } - - protected prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return this._proxy.$prepareCommandForTerminal(args, config); + return TPromise.wrap(this._proxy.$stopDASession(this._handle)); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts index 1f8d8af2c36..6d43cc159d3 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainContext, IExtHostContext, MainThreadDecorationsShape, ExtHostDecorationsShape, DecorationData, DecorationRequest } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IDecorationsService, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; -import { TPromise } from 'vs/base/common/winjs.base'; import { values } from 'vs/base/common/collections'; +import { CancellationToken } from 'vs/base/common/cancellation'; class DecorationRequestsQueue { private _idPool = 0; private _requests: { [id: number]: DecorationRequest } = Object.create(null); - private _resolver: { [id: number]: Function } = Object.create(null); + private _resolver: { [id: number]: (data: DecorationData) => any } = Object.create(null); private _timer: number; @@ -27,16 +27,18 @@ class DecorationRequestsQueue { // } - enqueue(handle: number, uri: URI): TPromise { + enqueue(handle: number, uri: URI, token: CancellationToken): Promise { const id = ++this._idPool; - return new TPromise((resolve, reject) => { + const result = new Promise(resolve => { this._requests[id] = { id, handle, uri }; this._resolver[id] = resolve; this._processQueue(); - }, () => { + }); + token.onCancellationRequested(() => { delete this._requests[id]; delete this._resolver[id]; }); + return result; } private _processQueue(): void { @@ -48,7 +50,7 @@ class DecorationRequestsQueue { // make request const requests = this._requests; const resolver = this._resolver; - this._proxy.$provideDecorations(values(requests)).then(data => { + this._proxy.$provideDecorations(values(requests), CancellationToken.None).then(data => { for (const id in resolver) { resolver[id](data[id]); } @@ -87,8 +89,8 @@ export class MainThreadDecorations implements MainThreadDecorationsShape { const registration = this._decorationsService.registerDecorationsProvider({ label, onDidChange: emitter.event, - provideDecorations: (uri) => { - return this._requestQueue.enqueue(handle, uri).then(data => { + provideDecorations: (uri, token) => { + return this._requestQueue.enqueue(handle, uri, token).then(data => { if (!data) { return undefined; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/electron-browser/mainThreadDiagnostics.ts index 3167a1a9037..343057b2703 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDiagnostics.ts @@ -5,7 +5,7 @@ 'use strict'; import { IMarkerService, IMarkerData } from 'vs/platform/markers/common/markers'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { MainThreadDiagnosticsShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadDialogs.ts b/src/vs/workbench/api/electron-browser/mainThreadDialogs.ts index e43fa20ef2e..de567b45317 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDialogs.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDialogs.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { MainThreadDiaglogsShape, MainContext, IExtHostContext, MainThreadDialogOpenOptions, MainThreadDialogSaveOptions } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts b/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts index ccf9eeb2637..b7db1a22712 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocumentContentProviders.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { ITextModel, DefaultEndOfLine } from 'vs/editor/common/model'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { MainThreadDocumentContentProvidersShape, ExtHostContext, ExtHostDocumentContentProvidersShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { ExtHostContext, ExtHostDocumentContentProvidersShape, IExtHostContext, MainContext, MainThreadDocumentContentProvidersShape } from '../node/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadDocumentContentProviders) export class MainThreadDocumentContentProviders implements MainThreadDocumentContentProvidersShape { @@ -27,6 +29,7 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon @ITextModelService private readonly _textModelResolverService: ITextModelService, @IModeService private readonly _modeService: IModeService, @IModelService private readonly _modelService: IModelService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ICodeEditorService codeEditorService: ICodeEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentContentProviders); @@ -40,7 +43,7 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon $registerTextContentProvider(handle: number, scheme: string): void { this._resourceContentProvider[handle] = this._textModelResolverService.registerTextModelContentProvider(scheme, { - provideTextContent: (uri: URI): TPromise => { + provideTextContent: (uri: URI): Thenable => { return this._proxy.$provideTextDocumentContent(handle, uri).then(value => { if (typeof value === 'string') { const firstLineText = value.substr(0, 1 + value.search(/\r?\n/)); @@ -67,10 +70,11 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon return; } - const textBuffer = createTextBuffer(value, DefaultEndOfLine.CRLF); - - if (!model.equalsTextBuffer(textBuffer)) { - model.setValueFromTextBuffer(textBuffer); - } + this._editorWorkerService.computeMoreMinimalEdits(model.uri, [{ text: value, range: model.getFullModelRange() }]).then(edits => { + if (edits.length > 0) { + // use the evil-edit as these models show in readonly-editor only + model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + } + }, onUnexpectedError); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts b/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts index a85c61c9d4e..cf0f1d0f816 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { TextFileModelChangeEvent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFileService, FileOperation } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { ExtHostContext, MainThreadDocumentsShape, ExtHostDocumentsShape, IExtHostContext } from '../node/extHost.protocol'; @@ -119,12 +119,6 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } })); - this._toDispose.push(fileService.onAfterOperation(e => { - if (e.operation === FileOperation.MOVE) { - this._proxy.$onDidRename(e.resource, e.target.resource); - } - })); - this._modelToDisposeMap = Object.create(null); } @@ -175,11 +169,11 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { // --- from extension host process - $trySaveDocument(uri: UriComponents): TPromise { + $trySaveDocument(uri: UriComponents): Thenable { return this._textFileService.save(URI.revive(uri)); } - $tryOpenDocument(_uri: UriComponents): TPromise { + $tryOpenDocument(_uri: UriComponents): Thenable { const uri = URI.revive(_uri); if (!uri.scheme || !(uri.fsPath || uri.authority)) { return TPromise.wrapError(new Error(`Invalid uri. Scheme and authority or path must be set.`)); @@ -209,7 +203,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { }); } - $tryCreateDocument(options?: { language?: string, content?: string }): TPromise { + $tryCreateDocument(options?: { language?: string, content?: string }): Thenable { return this._doCreateUntitled(void 0, options ? options.language : void 0, options ? options.content : void 0); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts index 503b0a3254e..be02a99220b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts @@ -24,7 +24,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; namespace mapset { diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index b1a55b0dd2b..232670fbde7 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -10,7 +10,7 @@ import { localize } from 'vs/nls'; import { disposed } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { equals as objectEquals } from 'vs/base/common/objects'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -27,10 +27,12 @@ import { ExtHostContext, ExtHostEditorsShape, IExtHostContext, ITextDocumentShow import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import { MainThreadTextEditor } from './mainThreadEditor'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IFileService } from 'vs/platform/files/common/files'; export class MainThreadTextEditors implements MainThreadTextEditorsShape { + private static INSTANCE_COUNT: number = 0; + + private _instanceId: string; private _proxy: ExtHostEditorsShape; private _documentsAndEditors: MainThreadDocumentsAndEditors; private _toDispose: IDisposable[]; @@ -46,6 +48,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService ) { + this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); this._documentsAndEditors = documentsAndEditors; this._toDispose = []; @@ -57,6 +60,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._toDispose.push(this._editorService.onDidVisibleEditorsChange(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.push(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors())); + this._toDispose.push(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors())); this._registeredDecorationTypes = Object.create(null); } @@ -111,7 +115,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // --- from extension host process - $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): TPromise { + $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Thenable { const uri = URI.revive(resource); const editorOptions: ITextEditorOptions = { @@ -133,7 +137,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { }); } - $tryShowEditor(id: string, position?: EditorViewColumn): TPromise { + $tryShowEditor(id: string, position?: EditorViewColumn): Thenable { let mainThreadEditor = this._documentsAndEditors.getEditor(id); if (mainThreadEditor) { let model = mainThreadEditor.getModel(); @@ -145,7 +149,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return undefined; } - $tryHideEditor(id: string): TPromise { + $tryHideEditor(id: string): Thenable { let mainThreadEditor = this._documentsAndEditors.getEditor(id); if (mainThreadEditor) { let editors = this._editorService.visibleControls; @@ -158,7 +162,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return undefined; } - $trySetSelections(id: string, selections: ISelection[]): TPromise { + $trySetSelections(id: string, selections: ISelection[]): Thenable { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -166,7 +170,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return TPromise.as(null); } - $trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): TPromise { + $trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): Thenable { + key = `${this._instanceId}-${key}`; if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -174,7 +179,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return TPromise.as(null); } - $trySetDecorationsFast(id: string, key: string, ranges: number[]): TPromise { + $trySetDecorationsFast(id: string, key: string, ranges: number[]): Thenable { + key = `${this._instanceId}-${key}`; if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -182,7 +188,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return TPromise.as(null); } - $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise { + $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Thenable { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -190,7 +196,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return undefined; } - $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise { + $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Thenable { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -198,19 +204,19 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return TPromise.as(null); } - $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): TPromise { + $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Thenable { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } return TPromise.as(this._documentsAndEditors.getEditor(id).applyEdits(modelVersionId, edits, opts)); } - $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): TPromise { + $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): Thenable { const { edits } = reviveWorkspaceEditDto(dto); return this._bulkEditService.apply({ edits }, undefined).then(() => true, err => false); } - $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise { + $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): Thenable { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); } @@ -218,16 +224,18 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { + key = `${this._instanceId}-${key}`; this._registeredDecorationTypes[key] = true; this._codeEditorService.registerDecorationType(key, options); } $removeTextEditorDecorationType(key: string): void { + key = `${this._instanceId}-${key}`; delete this._registeredDecorationTypes[key]; this._codeEditorService.removeDecorationType(key); } - $getDiffInformation(id: string): TPromise { + $getDiffInformation(id: string): Thenable { const editor = this._documentsAndEditors.getEditor(id); if (!editor) { @@ -253,19 +261,24 @@ CommandsRegistry.registerCommand('_workbench.open', function (accessor: Services const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); const openerService = accessor.get(IOpenerService); - const fileService = accessor.get(IFileService); const [resource, options, position] = args; - if (fileService.canHandleResource(resource)) { - return editorService.openEditor({ resource, options }, viewColumnToEditorGroup(editorGroupService, position)).then(() => void 0); - } else { - // http://, https://, command:id - //todo@ben make this proper - return openerService.open(resource).then(_ => void 0); + if (options || typeof position === 'number') { + // use editor options or editor view column as a hint to use the editor service for opening + return editorService.openEditor({ resource, options }, viewColumnToEditorGroup(editorGroupService, position)).then(_ => void 0); } + + if (resource && resource.scheme === 'command') { + // do not allow to execute commands from here + return TPromise.as(void 0); + } + + // finally, delegate to opener service + return openerService.open(resource).then(_ => void 0); }); + CommandsRegistry.registerCommand('_workbench.diff', function (accessor: ServicesAccessor, args: [URI, URI, string, string, IEditorOptions, EditorViewColumn]) { const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index 81593724b68..911c23e4589 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -5,12 +5,12 @@ 'use strict'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions } from 'vs/platform/files/common/files'; +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 { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../node/extHost.protocol'; +import { LabelRules, ILabelService } from 'vs/platform/label/common/label'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { @@ -20,7 +20,8 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { constructor( extHostContext: IExtHostContext, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @ILabelService private readonly _labelService: ILabelService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem); } @@ -39,6 +40,10 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { this._fileProvider.delete(handle); } + $setUriFormatter(scheme: string, formatter: LabelRules): void { + this._labelService.registerFormatter(scheme, formatter); + } + $onFileSystemChange(handle: number, changes: IFileChangeDto[]): void { this._fileProvider.get(handle).$onFileSystemChange(changes); } @@ -71,11 +76,9 @@ class RemoteFileSystemProvider implements IFileSystemProvider { watch(resource: URI, opts: IWatchOptions) { const session = Math.random(); this._proxy.$watch(this._handle, session, resource, opts); - return { - dispose: () => { - this._proxy.$unwatch(this._handle, session); - } - }; + return toDisposable(() => { + this._proxy.$unwatch(this._handle, session); + }); } $onFileSystemChange(changes: IFileChangeDto[]): void { @@ -88,42 +91,57 @@ class RemoteFileSystemProvider implements IFileSystemProvider { // --- forwarding calls - stat(resource: URI): TPromise { + private static _asBuffer(data: Uint8Array): Buffer { + return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); + } + + stat(resource: URI): Thenable { return this._proxy.$stat(this._handle, resource).then(undefined, err => { throw err; }); } - readFile(resource: URI): TPromise { - return this._proxy.$readFile(this._handle, resource).then(encoded => { - return Buffer.from(encoded, 'base64'); - }); + readFile(resource: URI): Thenable { + return this._proxy.$readFile(this._handle, resource); } - writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): TPromise { - let encoded = Buffer.isBuffer(content) - ? content.toString('base64') - : Buffer.from(content.buffer, content.byteOffset, content.byteLength).toString('base64'); - return this._proxy.$writeFile(this._handle, resource, encoded, opts); + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Thenable { + return this._proxy.$writeFile(this._handle, resource, RemoteFileSystemProvider._asBuffer(content), opts); } - delete(resource: URI): TPromise { - return this._proxy.$delete(this._handle, resource); + delete(resource: URI, opts: FileDeleteOptions): Thenable { + return this._proxy.$delete(this._handle, resource, opts); } - mkdir(resource: URI): TPromise { + mkdir(resource: URI): Thenable { return this._proxy.$mkdir(this._handle, resource); } - readdir(resource: URI): TPromise<[string, FileType][], any> { + readdir(resource: URI): Thenable<[string, FileType][]> { return this._proxy.$readdir(this._handle, resource); } - rename(resource: URI, target: URI, opts: FileOverwriteOptions): TPromise { + rename(resource: URI, target: URI, opts: FileOverwriteOptions): Thenable { return this._proxy.$rename(this._handle, resource, target, opts); } - copy(resource: URI, target: URI, opts: FileOverwriteOptions): TPromise { + copy(resource: URI, target: URI, opts: FileOverwriteOptions): Thenable { return this._proxy.$copy(this._handle, resource, target, opts); } + + open(resource: URI): Thenable { + return this._proxy.$open(this._handle, resource); + } + + close(fd: number): Thenable { + return this._proxy.$close(this._handle, fd); + } + + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Thenable { + return this._proxy.$read(this._handle, fd, pos, RemoteFileSystemProvider._asBuffer(data), offset, length); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Thenable { + return this._proxy.$write(this._handle, fd, pos, RemoteFileSystemProvider._asBuffer(data), offset, length); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystemEventService.ts index cda620bf166..5b6e9cf18be 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystemEventService.ts @@ -4,29 +4,32 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; -import { ExtHostContext, ExtHostFileSystemEventServiceShape, FileSystemEvents, IExtHostContext } from '../node/extHost.protocol'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { FileChangeType, IFileService, FileOperation } from 'vs/platform/files/common/files'; import { extHostCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../node/extHost.protocol'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @extHostCustomer export class MainThreadFileSystemEventService { - private readonly _listener: IDisposable; + private readonly _listener = new Array(); constructor( extHostContext: IExtHostContext, - @IFileService fileService: IFileService + @IFileService fileService: IFileService, + @ITextFileService textfileService: ITextFileService, ) { - const proxy: ExtHostFileSystemEventServiceShape = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); + const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); + + // file system events - (changes the editor and other make) const events: FileSystemEvents = { created: [], changed: [], deleted: [] }; - - this._listener = fileService.onFileChanges(event => { + fileService.onFileChanges(event => { for (let change of event.changes) { switch (change.type) { case FileChangeType.ADDED: @@ -45,10 +48,22 @@ export class MainThreadFileSystemEventService { events.created.length = 0; events.changed.length = 0; events.deleted.length = 0; - }); + }, undefined, this._listener); + + // file operation events - (changes the editor makes) + fileService.onAfterOperation(e => { + if (e.operation === FileOperation.MOVE) { + proxy.$onFileRename(e.resource, e.target.resource); + } + }, undefined, this._listener); + + textfileService.onWillMove(e => { + let promise = proxy.$onWillRename(e.oldResource, e.newResource); + e.waitUntil(promise); + }, undefined, this._listener); } dispose(): void { - this._listener.dispose(); + dispose(this._listener); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index cd3f6607fe2..98ba77076ab 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -4,24 +4,22 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { ITextModel, ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; -import { WorkspaceSymbolProviderRegistry, IWorkspaceSymbolProvider } from 'vs/workbench/parts/search/common/search'; -import { wireCancellationToken } from 'vs/base/common/async'; +import * as search from 'vs/workbench/parts/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, SymbolInformationDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto } from '../node/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IHeapService } from './mainThreadHeapService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) @@ -72,20 +70,31 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } } - private static _reviveSymbolInformationDto(data: SymbolInformationDto): modes.SymbolInformation; - private static _reviveSymbolInformationDto(data: SymbolInformationDto[]): modes.SymbolInformation[]; - private static _reviveSymbolInformationDto(data: SymbolInformationDto | SymbolInformationDto[]): modes.SymbolInformation | modes.SymbolInformation[] { + private static _reviveDefinitionLinkDto(data: DefinitionLinkDto): modes.DefinitionLink; + private static _reviveDefinitionLinkDto(data: DefinitionLinkDto[]): modes.DefinitionLink[]; + private static _reviveDefinitionLinkDto(data: DefinitionLinkDto | DefinitionLinkDto[]): modes.DefinitionLink | modes.DefinitionLink[] { if (!data) { - return data; + return data; } else if (Array.isArray(data)) { - data.forEach(MainThreadLanguageFeatures._reviveSymbolInformationDto); - return data; + data.forEach(l => MainThreadLanguageFeatures._reviveDefinitionLinkDto(l)); + return data; + } else { + data.uri = URI.revive(data.uri); + return data; + } + } + + private static _reviveWorkspaceSymbolDto(data: WorkspaceSymbolDto): search.IWorkspaceSymbol; + private static _reviveWorkspaceSymbolDto(data: WorkspaceSymbolDto[]): search.IWorkspaceSymbol[]; + private static _reviveWorkspaceSymbolDto(data: WorkspaceSymbolDto | WorkspaceSymbolDto[]): search.IWorkspaceSymbol | search.IWorkspaceSymbol[] { + if (!data) { + return data; + } else if (Array.isArray(data)) { + data.forEach(MainThreadLanguageFeatures._reviveWorkspaceSymbolDto); + return data; } else { data.location = MainThreadLanguageFeatures._reviveLocationDto(data.location); - if (data.children) { - data.children.forEach(MainThreadLanguageFeatures._reviveSymbolInformationDto); - } - return data; + return data; } } @@ -100,11 +109,11 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- outline - $registerOutlineSupport(handle: number, selector: ISerializedDocumentFilter[], extensionId: string): void { + $registerOutlineSupport(handle: number, selector: ISerializedDocumentFilter[], displayName: string): void { this._registrations[handle] = modes.DocumentSymbolProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { - extensionId, - provideDocumentSymbols: (model: ITextModel, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideDocumentSymbols(handle, model.uri)).then(MainThreadLanguageFeatures._reviveSymbolInformationDto); + displayName, + provideDocumentSymbols: (model: ITextModel, token: CancellationToken): Thenable => { + return this._proxy.$provideDocumentSymbols(handle, model.uri, token); } }); } @@ -115,10 +124,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha const provider = { provideCodeLenses: (model: ITextModel, token: CancellationToken): modes.ICodeLensSymbol[] | Thenable => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeLenses(handle, model.uri))); + return this._heapService.trackRecursive(this._proxy.$provideCodeLenses(handle, model.uri, token)); }, resolveCodeLens: (model: ITextModel, codeLens: modes.ICodeLensSymbol, token: CancellationToken): modes.ICodeLensSymbol | Thenable => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$resolveCodeLens(handle, model.uri, codeLens))); + return this._heapService.trackRecursive(this._proxy.$resolveCodeLens(handle, model.uri, codeLens, token)); } }; @@ -142,8 +151,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerDeclaractionSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.DefinitionProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { - provideDefinition: (model, position, token): Thenable => { - return wireCancellationToken(token, this._proxy.$provideDefinition(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveLocationDto); + provideDefinition: (model, position, token): Thenable => { + return this._proxy.$provideDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveDefinitionLinkDto); } }); } @@ -151,7 +160,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerImplementationSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.ImplementationProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideImplementation: (model, position, token): Thenable => { - return wireCancellationToken(token, this._proxy.$provideImplementation(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveLocationDto); + return this._proxy.$provideImplementation(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveDefinitionLinkDto); } }); } @@ -159,7 +168,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerTypeDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.TypeDefinitionProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideTypeDefinition: (model, position, token): Thenable => { - return wireCancellationToken(token, this._proxy.$provideTypeDefinition(handle, model.uri, position)).then(MainThreadLanguageFeatures._reviveLocationDto); + return this._proxy.$provideTypeDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveDefinitionLinkDto); } }); } @@ -169,7 +178,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerHoverProvider(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.HoverProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideHover: (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideHover(handle, model.uri, position)); + return this._proxy.$provideHover(handle, model.uri, position, token); } }); } @@ -179,7 +188,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerDocumentHighlightProvider(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.DocumentHighlightProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideDocumentHighlights: (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideDocumentHighlights(handle, model.uri, position)); + return this._proxy.$provideDocumentHighlights(handle, model.uri, position, token); } }); } @@ -189,7 +198,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerReferenceSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.ReferenceProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideReferences: (model: ITextModel, position: EditorPosition, context: modes.ReferenceContext, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideReferences(handle, model.uri, position, context)).then(MainThreadLanguageFeatures._reviveLocationDto); + return this._proxy.$provideReferences(handle, model.uri, position, context, token).then(MainThreadLanguageFeatures._reviveLocationDto); } }); } @@ -199,7 +208,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[], providedCodeActionKinds?: string[]): void { this._registrations[handle] = modes.CodeActionProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideCodeActions: (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Thenable => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context))).then(MainThreadLanguageFeatures._reviveCodeActionDto); + return this._heapService.trackRecursive(this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token)).then(MainThreadLanguageFeatures._reviveCodeActionDto); }, providedCodeActionKinds }); @@ -210,7 +219,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerDocumentFormattingSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.DocumentFormattingEditProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideDocumentFormattingEdits: (model: ITextModel, options: modes.FormattingOptions, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideDocumentFormattingEdits(handle, model.uri, options)); + return this._proxy.$provideDocumentFormattingEdits(handle, model.uri, options, token); } }); } @@ -218,7 +227,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerRangeFormattingSupport(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.DocumentRangeFormattingEditProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideDocumentRangeFormattingEdits: (model: ITextModel, range: EditorRange, options: modes.FormattingOptions, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideDocumentRangeFormattingEdits(handle, model.uri, range, options)); + return this._proxy.$provideDocumentRangeFormattingEdits(handle, model.uri, range, options, token); } }); } @@ -229,7 +238,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha autoFormatTriggerCharacters, provideOnTypeFormattingEdits: (model: ITextModel, position: EditorPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideOnTypeFormattingEdits(handle, model.uri, position, ch, options)); + return this._proxy.$provideOnTypeFormattingEdits(handle, model.uri, position, ch, options, token); } }); } @@ -238,18 +247,18 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerNavigateTypeSupport(handle: number): void { let lastResultId: number; - this._registrations[handle] = WorkspaceSymbolProviderRegistry.register({ - provideWorkspaceSymbols: (search: string): TPromise => { - return this._proxy.$provideWorkspaceSymbols(handle, search).then(result => { + this._registrations[handle] = search.WorkspaceSymbolProviderRegistry.register({ + provideWorkspaceSymbols: (search: string, token: CancellationToken): Thenable => { + return this._proxy.$provideWorkspaceSymbols(handle, search, token).then(result => { if (lastResultId !== undefined) { this._proxy.$releaseWorkspaceSymbols(handle, lastResultId); } lastResultId = result._id; - return MainThreadLanguageFeatures._reviveSymbolInformationDto(result.symbols); + return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(result.symbols); }); }, - resolveWorkspaceSymbol: (item: modes.SymbolInformation): TPromise => { - return this._proxy.$resolveWorkspaceSymbol(handle, item).then(i => MainThreadLanguageFeatures._reviveSymbolInformationDto(i)); + resolveWorkspaceSymbol: (item: search.IWorkspaceSymbol, token: CancellationToken): Thenable => { + return this._proxy.$resolveWorkspaceSymbol(handle, item, token).then(i => MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(i)); } }); } @@ -260,10 +269,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations[handle] = modes.RenameProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(reviveWorkspaceEditDto); + return this._proxy.$provideRenameEdits(handle, model.uri, position, newName, token).then(reviveWorkspaceEditDto); }, resolveRenameLocation: supportResolveLocation - ? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => wireCancellationToken(token, this._proxy.$resolveRenameLocation(handle, model.uri, position)) + ? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => this._proxy.$resolveRenameLocation(handle, model.uri, position, token) : undefined }); } @@ -274,7 +283,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations[handle] = modes.SuggestRegistry.register(typeConverters.LanguageSelector.from(selector), { triggerCharacters, provideCompletionItems: (model: ITextModel, position: EditorPosition, context: modes.SuggestContext, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideCompletionItems(handle, model.uri, position, context)).then(result => { + return this._proxy.$provideCompletionItems(handle, model.uri, position, context, token).then(result => { if (!result) { return result; } @@ -286,7 +295,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }); }, resolveCompletionItem: supportsResolveDetails - ? (model, position, suggestion, token) => wireCancellationToken(token, this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion)) + ? (model, position, suggestion, token) => this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion, token) : undefined }); } @@ -298,10 +307,9 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha signatureHelpTriggerCharacters: triggerCharacter, - provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideSignatureHelp(handle, model.uri, position)); + provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken, context: modes.SignatureHelpContext): Thenable => { + return this._proxy.$provideSignatureHelp(handle, model.uri, position, context, token); } - }); } @@ -310,10 +318,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerDocumentLinkProvider(handle: number, selector: ISerializedDocumentFilter[]): void { this._registrations[handle] = modes.LinkProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideLinks: (model, token) => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideDocumentLinks(handle, model.uri))); + return this._heapService.trackRecursive(this._proxy.$provideDocumentLinks(handle, model.uri, token)); }, resolveLink: (link, token) => { - return wireCancellationToken(token, this._proxy.$resolveDocumentLink(handle, link)); + return this._proxy.$resolveDocumentLink(handle, link, token); } }); } @@ -324,7 +332,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha const proxy = this._proxy; this._registrations[handle] = modes.ColorProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideDocumentColors: (model, token) => { - return wireCancellationToken(token, proxy.$provideDocumentColors(handle, model.uri)) + return proxy.$provideDocumentColors(handle, model.uri, token) .then(documentColors => { return documentColors.map(documentColor => { const [red, green, blue, alpha] = documentColor.color; @@ -344,10 +352,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }, provideColorPresentations: (model, colorInfo, token) => { - return wireCancellationToken(token, proxy.$provideColorPresentations(handle, model.uri, { + return proxy.$provideColorPresentations(handle, model.uri, { color: [colorInfo.color.red, colorInfo.color.green, colorInfo.color.blue, colorInfo.color.alpha], range: colorInfo.range - })); + }, token); } }); } @@ -358,7 +366,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha const proxy = this._proxy; this._registrations[handle] = modes.FoldingRangeProviderRegistry.register(typeConverters.LanguageSelector.from(selector), { provideFoldingRanges: (model, context, token) => { - return wireCancellationToken(token, proxy.$provideFoldingRanges(handle, model.uri, context)); + return proxy.$provideFoldingRanges(handle, model.uri, context, token); } }); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguages.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguages.ts index 31e69556c69..d79b1864fbb 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguages.ts @@ -5,7 +5,9 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelService } from 'vs/editor/common/services/modelService'; import { MainThreadLanguagesShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -13,18 +15,36 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC export class MainThreadLanguages implements MainThreadLanguagesShape { private _modeService: IModeService; + private _modelService: IModelService; constructor( extHostContext: IExtHostContext, - @IModeService modeService: IModeService + @IModeService modeService: IModeService, + @IModelService modelService: IModelService ) { this._modeService = modeService; + this._modelService = modelService; } public dispose(): void { } - $getLanguages(): TPromise { + $getLanguages(): Thenable { return TPromise.as(this._modeService.getRegisteredModes()); } + + $changeLanguage(resource: UriComponents, languageId: string): Thenable { + const uri = URI.revive(resource); + let model = this._modelService.getModel(uri); + if (!model) { + return TPromise.wrapError(new Error('Invalid uri')); + } + return this._modeService.getOrCreateMode(languageId).then(mode => { + if (mode.getId() !== languageId) { + return TPromise.wrapError(new Error(`Unknown language id: ${languageId}`)); + } + this._modelService.setMode(model, mode); + return undefined; + }); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadOutputService.ts b/src/vs/workbench/api/electron-browser/mainThreadOutputService.ts index 905dc07490d..9af44b16a08 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadOutputService.ts @@ -11,10 +11,13 @@ import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { MainThreadOutputServiceShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { UriComponents, URI } from 'vs/base/common/uri'; @extHostNamedCustomer(MainContext.MainThreadOutputService) export class MainThreadOutputService implements MainThreadOutputServiceShape { + private static _idPool = 1; + private readonly _outputService: IOutputService; private readonly _partService: IPartService; private readonly _panelService: IPanelService; @@ -34,31 +37,37 @@ export class MainThreadOutputService implements MainThreadOutputServiceShape { // Leave all the existing channels intact (e.g. might help with troubleshooting) } - public $append(channelId: string, label: string, value: string): TPromise { - this._getChannel(channelId, label).append(value); - return undefined; + public $register(label: string, log: boolean, file?: UriComponents): Thenable { + const id = 'extension-output-#' + (MainThreadOutputService._idPool++); + Registry.as(Extensions.OutputChannels).registerChannel({ id, label, file: file ? URI.revive(file) : null, log }); + return TPromise.as(id); } - public $clear(channelId: string, label: string): TPromise { - this._getChannel(channelId, label).clear(); - return undefined; - } - - public $reveal(channelId: string, label: string, preserveFocus: boolean): TPromise { - const channel = this._getChannel(channelId, label); - this._outputService.showChannel(channel.id, preserveFocus); - return undefined; - } - - private _getChannel(channelId: string, label: string): IOutputChannel { - if (!Registry.as(Extensions.OutputChannels).getChannel(channelId)) { - Registry.as(Extensions.OutputChannels).registerChannel(channelId, label); + public $append(channelId: string, value: string): Thenable { + const channel = this._getChannel(channelId); + if (channel) { + channel.append(value); } - - return this._outputService.getChannel(channelId); + return undefined; } - public $close(channelId: string): TPromise { + public $clear(channelId: string): Thenable { + const channel = this._getChannel(channelId); + if (channel) { + channel.clear(); + } + return undefined; + } + + public $reveal(channelId: string, preserveFocus: boolean): Thenable { + const channel = this._getChannel(channelId); + if (channel) { + this._outputService.showChannel(channel.id, preserveFocus); + } + return undefined; + } + + public $close(channelId: string): Thenable { const panel = this._panelService.getActivePanel(); if (panel && panel.getId() === OUTPUT_PANEL_ID && channelId === this._outputService.getActiveChannel().id) { return this._partService.setPanelHidden(true); @@ -67,8 +76,15 @@ export class MainThreadOutputService implements MainThreadOutputServiceShape { return undefined; } - public $dispose(channelId: string, label: string): TPromise { - this._getChannel(channelId, label).dispose(); + public $dispose(channelId: string): Thenable { + const channel = this._getChannel(channelId); + if (channel) { + channel.dispose(); + } return undefined; } + + private _getChannel(channelId: string): IOutputChannel { + return this._outputService.getChannel(channelId); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadProgress.ts b/src/vs/workbench/api/electron-browser/mainThreadProgress.ts index 2aa8a46d301..8acaf3865c0 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadProgress.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadProgress.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IProgressService2, IProgress, IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; +import { IProgress } from 'vs/platform/progress/common/progress'; import { MainThreadProgressShape, MainContext, IExtHostContext, ExtHostProgressShape, ExtHostContext } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IProgressService2, IProgressStep, IProgressOptions } from 'vs/workbench/services/progress/common/progress'; @extHostNamedCustomer(MainContext.MainThreadProgress) export class MainThreadProgress implements MainThreadProgressShape { diff --git a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts index e98293c8a8a..f6f1c5fcf87 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts @@ -5,16 +5,16 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { asWinJsPromise } from 'vs/base/common/async'; import { IPickOptions, IInputOptions, IQuickInputService, IQuickInput } from 'vs/platform/quickinput/common/quickInput'; -import { InputBoxOptions, CancellationToken } from 'vscode'; -import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, MainContext, IExtHostContext } from '../node/extHost.protocol'; +import { InputBoxOptions } from 'vscode'; +import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, TransferQuickPickItems, MainContext, IExtHostContext, TransferQuickInput, TransferQuickInputButton } from 'vs/workbench/api/node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; -interface MultiStepSession { - handle: number; +interface QuickInputSession { input: IQuickInput; - token: CancellationToken; + handlesToItems: Map; } @extHostNamedCustomer(MainContext.MainThreadQuickOpen) @@ -22,11 +22,10 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { private _proxy: ExtHostQuickOpenShape; private _quickInputService: IQuickInputService; - private _doSetItems: (items: MyQuickPickItems[]) => any; + private _doSetItems: (items: TransferQuickPickItems[]) => any; private _doSetError: (error: Error) => any; - private _contents: TPromise; + private _contents: TPromise; private _token: number = 0; - private _multiStep: MultiStepSession; constructor( extHostContext: IExtHostContext, @@ -39,17 +38,10 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { public dispose(): void { } - $show(multiStepHandle: number | undefined, options: IPickOptions): TPromise { - - const multiStep = typeof multiStepHandle === 'number'; - if (multiStep && !(this._multiStep && multiStepHandle === this._multiStep.handle && !this._multiStep.token.isCancellationRequested)) { - return TPromise.as(undefined); - } - const input: IQuickInput = multiStep ? this._multiStep.input : this._quickInputService; - + $show(options: IPickOptions, token: CancellationToken): Thenable { const myToken = ++this._token; - this._contents = new TPromise((c, e) => { + this._contents = new TPromise((c, e) => { this._doSetItems = (items) => { if (myToken === this._token) { c(items); @@ -63,39 +55,40 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { }; }); + options = { + ...options, + onDidFocus: el => { + if (el) { + this._proxy.$onItemSelected((el).handle); + } + } + }; + if (options.canPickMany) { - return asWinJsPromise(token => input.pick(this._contents, options as { canPickMany: true }, token)).then(items => { + return this._quickInputService.pick(this._contents, options as { canPickMany: true }, token).then(items => { if (items) { return items.map(item => item.handle); } return undefined; - }, undefined, progress => { - if (progress) { - this._proxy.$onItemSelected((progress).handle); - } }); } else { - return asWinJsPromise(token => input.pick(this._contents, options, token)).then(item => { + return this._quickInputService.pick(this._contents, options, token).then(item => { if (item) { return item.handle; } return undefined; - }, undefined, progress => { - if (progress) { - this._proxy.$onItemSelected((progress).handle); - } }); } } - $setItems(items: MyQuickPickItems[]): TPromise { + $setItems(items: TransferQuickPickItems[]): Thenable { if (this._doSetItems) { this._doSetItems(items); } return undefined; } - $setError(error: Error): TPromise { + $setError(error: Error): Thenable { if (this._doSetError) { this._doSetError(error); } @@ -104,14 +97,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { // ---- input - $input(multiStepHandle: number | undefined, options: InputBoxOptions, validateInput: boolean): TPromise { - - const multiStep = typeof multiStepHandle === 'number'; - if (multiStep && !(this._multiStep && multiStepHandle === this._multiStep.handle && !this._multiStep.token.isCancellationRequested)) { - return TPromise.as(undefined); - } - const input: IQuickInput = multiStep ? this._multiStep.input : this._quickInputService; - + $input(options: InputBoxOptions, validateInput: boolean, token: CancellationToken): Thenable { const inputOptions: IInputOptions = Object.create(null); if (options) { @@ -129,27 +115,111 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { }; } - return asWinJsPromise(token => input.input(inputOptions, token)); + return this._quickInputService.input(inputOptions, token); } - // ---- Multi-step input + // ---- QuickInput - $multiStep(handle: number): TPromise { - let outerReject: (err: any) => void; - let innerResolve: (value: void) => void; - const promise = new TPromise((_, rej) => outerReject = rej, () => innerResolve(undefined)); - this._quickInputService.multiStepInput((input, token) => { - this._multiStep = { handle, input, token }; - const promise = new TPromise(res => innerResolve = res); - token.onCancellationRequested(() => innerResolve(undefined)); - return promise; - }) - .then(() => promise.cancel(), err => outerReject(err)) - .then(() => { - if (this._multiStep && this._multiStep.handle === handle) { - this._multiStep = null; + private sessions = new Map(); + + $createOrUpdate(params: TransferQuickInput): Thenable { + const sessionId = params.id; + let session = this.sessions.get(sessionId); + if (!session) { + if (params.type === 'quickPick') { + const input = this._quickInputService.createQuickPick(); + input.onDidAccept(() => { + this._proxy.$onDidAccept(sessionId); + }); + input.onDidChangeActive(items => { + this._proxy.$onDidChangeActive(sessionId, items.map(item => (item as TransferQuickPickItems).handle)); + }); + input.onDidChangeSelection(items => { + this._proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItems).handle)); + }); + input.onDidTriggerButton(button => { + this._proxy.$onDidTriggerButton(sessionId, (button as TransferQuickInputButton).handle); + }); + input.onDidChangeValue(value => { + this._proxy.$onDidChangeValue(sessionId, value); + }); + input.onDidHide(() => { + this._proxy.$onDidHide(sessionId); + }); + session = { + input, + handlesToItems: new Map() + }; + } else { + const input = this._quickInputService.createInputBox(); + input.onDidAccept(() => { + this._proxy.$onDidAccept(sessionId); + }); + input.onDidTriggerButton(button => { + this._proxy.$onDidTriggerButton(sessionId, (button as TransferQuickInputButton).handle); + }); + input.onDidChangeValue(value => { + this._proxy.$onDidChangeValue(sessionId, value); + }); + input.onDidHide(() => { + this._proxy.$onDidHide(sessionId); + }); + session = { + input, + handlesToItems: new Map() + }; + } + this.sessions.set(sessionId, session); + } + const { input, handlesToItems } = session; + for (const param in params) { + if (param === 'id' || param === 'type') { + continue; + } + if (param === 'visible') { + if (params.visible) { + input.show(); + } else { + input.hide(); } - }); - return promise; + } else if (param === 'items') { + handlesToItems.clear(); + params[param].forEach(item => { + handlesToItems.set(item.handle, item); + }); + input[param] = params[param]; + } else if (param === 'activeItems' || param === 'selectedItems') { + input[param] = params[param] + .filter(handle => handlesToItems.has(handle)) + .map(handle => handlesToItems.get(handle)); + } else if (param === 'buttons') { + input[param] = params.buttons.map(button => { + if (button.handle === -1) { + return this._quickInputService.backButton; + } + const { iconPath, tooltip, handle } = button; + return { + iconPath: { + dark: URI.revive(iconPath.dark), + light: iconPath.light && URI.revive(iconPath.light) + }, + tooltip, + handle + }; + }); + } else { + input[param] = params[param]; + } + } + return TPromise.as(undefined); + } + + $dispose(sessionId: number): Thenable { + const session = this.sessions.get(sessionId); + if (session) { + session.input.dispose(); + this.sessions.delete(sessionId); + } + return TPromise.as(undefined); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index 6bd43da77dc..e26b516c369 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -6,8 +6,8 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { Event, Emitter, debounceEvent } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation } from 'vs/workbench/services/scm/common/scm'; @@ -15,6 +15,7 @@ import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeature import { Command } from 'vs/editor/common/modes'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ISplice, Sequence } from 'vs/base/common/sequence'; +import { CancellationToken } from 'vs/base/common/cancellation'; class MainThreadSCMResourceGroup implements ISCMResourceGroup { @@ -73,7 +74,7 @@ class MainThreadSCMResource implements ISCMResource { public decorations: ISCMResourceDecorations ) { } - open(): TPromise { + open(): Thenable { return this.proxy.$executeResourceCommand(this.sourceControlHandle, this.groupHandle, this.handle); } @@ -241,7 +242,7 @@ class MainThreadSCMProvider implements ISCMProvider { return TPromise.as(null); } - return this.proxy.$provideOriginalResource(this.handle, uri) + return TPromise.wrap(this.proxy.$provideOriginalResource(this.handle, uri, CancellationToken.None)) .then(result => result && URI.revive(result)); } @@ -270,6 +271,9 @@ export class MainThreadSCM implements MainThreadSCMShape { @ISCMService private scmService: ISCMService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); + + debounceEvent(scmService.onDidChangeSelectedRepositories, (_, e) => e, 100) + (this.onDidChangeSelectedRepositories, this, this._disposables); } dispose(): void { @@ -401,20 +405,28 @@ export class MainThreadSCM implements MainThreadSCMShape { } if (enabled) { - repository.input.validateInput = async (value, pos): TPromise => { - const result = await this._proxy.$validateInput(sourceControlHandle, value, pos); + repository.input.validateInput = (value, pos): TPromise => { + return TPromise.wrap(this._proxy.$validateInput(sourceControlHandle, value, pos).then(result => { + if (!result) { + return undefined; + } - if (!result) { - return undefined; - } - - return { - message: result[0], - type: result[1] - }; + return { + message: result[0], + type: result[1] + }; + })); }; } else { repository.input.validateInput = () => TPromise.as(undefined); } } + + private onDidChangeSelectedRepositories(repositories: ISCMRepository[]): void { + const handles = repositories + .filter(r => r.provider instanceof MainThreadSCMProvider) + .map(r => (r.provider as MainThreadSCMProvider).handle); + + this._proxy.$setSelectedSourceControls(handles); + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index 6f10e6490be..2055bda2c70 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -24,7 +24,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { extHostCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService2, ProgressLocation } from 'vs/workbench/services/progress/common/progress'; import { localize } from 'vs/nls'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; @@ -37,6 +37,7 @@ import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { ICodeActionsOnSaveOptions } from 'vs/editor/common/config/editorOptions'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export interface ISaveParticipantParticipant extends ISaveParticipant { // progressMessage: string; @@ -212,19 +213,24 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { const versionNow = model.getVersionId(); const { tabSize, insertSpaces } = model.getOptions(); - const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() }); + const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() }); return new Promise((resolve, reject) => { - setTimeout(() => reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)), timeout); - getDocumentFormattingEdits(model, { tabSize, insertSpaces }) - .then(edits => this._editorWorkerService.computeMoreMinimalEdits(model.uri, edits)) - .then(resolve, err => { - if (!(err instanceof Error) || err.name !== NoProviderError.Name) { - reject(err); - } else { - resolve(); - } - }); + let source = new CancellationTokenSource(); + let request = getDocumentFormattingEdits(model, { tabSize, insertSpaces }, source.token); + + setTimeout(() => { + reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); + source.cancel(); + }, timeout); + + request.then(edits => this._editorWorkerService.computeMoreMinimalEdits(model.uri, edits)).then(resolve, err => { + if (!(err instanceof Error) || err.name !== NoProviderError.Name) { + reject(err); + } else { + resolve(); + } + }); }).then(edits => { if (!isFalsyOrEmpty(edits) && versionNow === model.getVersionId()) { diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts index 2fc636b81e1..e2f5950e12e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -5,14 +5,15 @@ 'use strict'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { IFileMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, QueryType, IRawFileMatch2, ISearchCompleteStats } from 'vs/platform/search/common/search'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IFileMatch, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, QueryType, SearchProviderType } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../node/extHost.protocol'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { CancellationToken } from 'vs/base/common/cancellation'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -29,12 +30,20 @@ export class MainThreadSearch implements MainThreadSearchShape { } dispose(): void { - this._searchProvider.forEach(value => dispose()); + this._searchProvider.forEach(value => value.dispose()); this._searchProvider.clear(); } - $registerSearchProvider(handle: number, scheme: string): void { - this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, scheme, handle, this._proxy)); + $registerTextSearchProvider(handle: number, scheme: string): void { + this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.text, scheme, handle, this._proxy)); + } + + $registerFileSearchProvider(handle: number, scheme: string): void { + this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.file, scheme, handle, this._proxy)); + } + + $registerFileIndexProvider(handle: number, scheme: string): void { + this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.fileIndex, scheme, handle, this._proxy)); } $unregisterProvider(handle: number): void { @@ -42,7 +51,11 @@ export class MainThreadSearch implements MainThreadSearchShape { this._searchProvider.delete(handle); } - $handleFindMatch(handle: number, session, data: UriComponents | IRawFileMatch2[]): void { + $handleFileMatch(handle: number, session, data: UriComponents[]): void { + this._searchProvider.get(handle).handleFindMatch(session, data); + } + + $handleTextMatch(handle: number, session, data: IRawFileMatch2[]): void { this._searchProvider.get(handle).handleFindMatch(session, data); } @@ -66,7 +79,7 @@ class SearchOperation { addMatch(match: IFileMatch): void { if (this.matches.has(match.resource.toString())) { // Merge with previous IFileMatches - this.matches.get(match.resource.toString()).lineMatches.push(...match.lineMatches); + this.matches.get(match.resource.toString()).matches.push(...match.matches); } else { this.matches.set(match.resource.toString(), match); } @@ -75,81 +88,68 @@ class SearchOperation { } } -class RemoteSearchProvider implements ISearchResultProvider { +class RemoteSearchProvider implements ISearchResultProvider, IDisposable { private readonly _registrations: IDisposable[]; private readonly _searches = new Map(); constructor( searchService: ISearchService, + type: SearchProviderType, private readonly _scheme: string, private readonly _handle: number, private readonly _proxy: ExtHostSearchShape ) { - this._registrations = [searchService.registerSearchResultProvider(this)]; + this._registrations = [searchService.registerSearchResultProvider(this._scheme, type, this)]; } dispose(): void { dispose(this._registrations); } - search(query: ISearchQuery): PPromise { - + search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): TPromise { if (isFalsyOrEmpty(query.folderQueries)) { - return PPromise.as(undefined); + return TPromise.as(undefined); } - const folderQueriesForScheme = query.folderQueries.filter(fq => fq.folder.scheme === this._scheme); - if (!folderQueriesForScheme.length) { - return TPromise.wrap(null); - } + const search = new SearchOperation(onProgress); + this._searches.set(search.id, search); - query = { - ...query, - folderQueries: folderQueriesForScheme - }; + const searchP = query.type === QueryType.File + ? this._proxy.$provideFileSearchResults(this._handle, search.id, query, token) + : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, query, token); - let outer: TPromise; - - return new PPromise((resolve, reject, report) => { - - const search = new SearchOperation(report); - this._searches.set(search.id, search); - - outer = query.type === QueryType.File - ? this._proxy.$provideFileSearchResults(this._handle, search.id, query) - : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, query); - - outer.then((result: ISearchCompleteStats) => { - this._searches.delete(search.id); - resolve(({ results: values(search.matches), stats: result.stats, limitHit: result.limitHit })); - }, err => { - this._searches.delete(search.id); - reject(err); - }); - }, () => { - if (outer) { - outer.cancel(); - } + return TPromise.wrap(searchP).then((result: ISearchCompleteStats) => { + this._searches.delete(search.id); + return { results: values(search.matches), stats: result.stats, limitHit: result.limitHit }; + }, err => { + this._searches.delete(search.id); + return TPromise.wrapError(err); }); } - handleFindMatch(session: number, dataOrUri: UriComponents | IRawFileMatch2[]): void { + clearCache(cacheKey: string): TPromise { + return TPromise.wrap(this._proxy.$clearCache(cacheKey)); + } + + handleFindMatch(session: number, dataOrUri: (UriComponents | IRawFileMatch2)[]): void { if (!this._searches.has(session)) { // ignore... return; } const searchOp = this._searches.get(session); - if (Array.isArray(dataOrUri)) { - dataOrUri.forEach(m => { + dataOrUri.forEach(result => { + if ((result).matches) { searchOp.addMatch({ - resource: URI.revive(m.resource), - lineMatches: m.lineMatches + resource: URI.revive((result).resource), + matches: (result).matches }); - }); - } else { - searchOp.addMatch({ resource: URI.revive(dataOrUri) }); - } + } else { + searchOp.addMatch({ + resource: URI.revive(result) + }); + } + }); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadStorage.ts b/src/vs/workbench/api/electron-browser/mainThreadStorage.ts index 403ddbdde6b..49d394106e9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadStorage.ts @@ -24,7 +24,7 @@ export class MainThreadStorage implements MainThreadStorageShape { dispose(): void { } - $getValue(shared: boolean, key: string): TPromise { + $getValue(shared: boolean, key: string): Thenable { let jsonValue = this._storageService.get(key, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); if (!jsonValue) { return TPromise.as(undefined); @@ -38,7 +38,7 @@ export class MainThreadStorage implements MainThreadStorageShape { } } - $setValue(shared: boolean, key: string, value: any): TPromise { + $setValue(shared: boolean, key: string, value: any): Thenable { let jsonValue: any; try { jsonValue = JSON.stringify(value); diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index ab2c0f3e367..b6f555f78b4 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -6,11 +6,13 @@ import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import * as Objects from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; +import { IStringDictionary } from 'vs/base/common/collections'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -37,10 +39,10 @@ namespace TaskExecutionDTO { task: TaskDTO.from(value.task) }; } - export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService): TaskExecution { + export function to(value: TaskExecutionDTO, workspace: IWorkspaceContextService, executeOnly: boolean): TaskExecution { return { id: value.id, - task: TaskDTO.to(value.task, workspace) + task: TaskDTO.to(value.task, workspace, executeOnly) }; } } @@ -69,8 +71,15 @@ namespace TaskDefinitionDTO { delete result._key; return result; } - export function to(value: TaskDefinitionDTO): KeyedTaskIdentifier { - return TaskDefinition.createTaskIdentifier(value, console); + export function to(value: TaskDefinitionDTO, executeOnly: boolean): KeyedTaskIdentifier { + let result = TaskDefinition.createTaskIdentifier(value, console); + if (result === void 0 && executeOnly) { + result = { + _key: generateUuid(), + type: '$executeOnly' + }; + } + return result; } } @@ -301,7 +310,7 @@ namespace TaskDTO { return result; } - export function to(task: TaskDTO, workspace: IWorkspaceContextService): Task { + export function to(task: TaskDTO, workspace: IWorkspaceContextService, executeOnly: boolean): Task { if (typeof task.name !== 'string') { return undefined; } @@ -320,10 +329,10 @@ namespace TaskDTO { let source = TaskSourceDTO.to(task.source, workspace); let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); - let definition = TaskDefinitionDTO.to(task.definition); + let definition = TaskDefinitionDTO.to(task.definition, executeOnly); let id = `${task.source.extensionId}.${definition._key}`; let result: ContributedTask = { - _id: id, // uuidMap.getUUID(identifier), + _id: id, // uuidMap.getUUID(identifier) _source: source, _label: label, type: definition.type, @@ -384,10 +393,10 @@ export class MainThreadTask implements MainThreadTaskShape { this._activeHandles = Object.create(null); } - public $registerTaskProvider(handle: number): TPromise { + public $registerTaskProvider(handle: number): Thenable { this._taskService.registerTaskProvider(handle, { - provideTasks: () => { - return this._proxy.$provideTasks(handle).then((value) => { + provideTasks: (validTypes: IStringDictionary) => { + return TPromise.wrap(this._proxy.$provideTasks(handle, validTypes)).then((value) => { let tasks: Task[] = []; for (let task of value.tasks) { let taskTransfer = task._source as any as ExtensionTaskSourceTransfer; @@ -414,13 +423,13 @@ export class MainThreadTask implements MainThreadTaskShape { return TPromise.wrap(undefined); } - public $unregisterTaskProvider(handle: number): TPromise { + public $unregisterTaskProvider(handle: number): Thenable { this._taskService.unregisterTaskProvider(handle); delete this._activeHandles[handle]; return TPromise.wrap(undefined); } - public $fetchTasks(filter?: TaskFilterDTO): TPromise { + public $fetchTasks(filter?: TaskFilterDTO): Thenable { return this._taskService.tasks(TaskFilterDTO.to(filter)).then((tasks) => { let result: TaskDTO[] = []; for (let task of tasks) { @@ -433,7 +442,7 @@ export class MainThreadTask implements MainThreadTaskShape { }); } - public $executeTask(value: TaskHandleDTO | TaskDTO): TPromise { + public $executeTask(value: TaskHandleDTO | TaskDTO): Thenable { return new TPromise((resolve, reject) => { if (TaskHandleDTO.is(value)) { let workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); @@ -448,7 +457,7 @@ export class MainThreadTask implements MainThreadTaskShape { reject(new Error('Task not found')); }); } else { - let task = TaskDTO.to(value, this._workspaceContextServer); + let task = TaskDTO.to(value, this._workspaceContextServer, true); this._taskService.run(task); let result: TaskExecutionDTO = { id: task._id, @@ -459,7 +468,7 @@ export class MainThreadTask implements MainThreadTaskShape { }); } - public $terminateTask(id: string): TPromise { + public $terminateTask(id: string): Thenable { return new TPromise((resolve, reject) => { this._taskService.getActiveTasks().then((tasks) => { for (let task of tasks) { @@ -494,12 +503,14 @@ export class MainThreadTask implements MainThreadTaskShape { } this._taskService.registerTaskSystem(key, { platform: platform, - fileSystemScheme: key, + uriProvider: (path: string): URI => { + return URI.parse(`${info.scheme}://${info.authority}${path}`); + }, context: this._extHostContext, resolveVariables: (workspaceFolder: IWorkspaceFolder, variables: Set): TPromise> => { let vars: string[] = []; variables.forEach(item => vars.push(item)); - return this._proxy.$resolveVariables(workspaceFolder.uri, vars).then(values => { + return TPromise.wrap(this._proxy.$resolveVariables(workspaceFolder.uri, vars)).then(values => { let result = new Map(); Object.keys(values).forEach(key => result.set(key, values[key])); return result; diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 58e3ab9786d..d9e2fe0ea58 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -5,9 +5,9 @@ 'use strict'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from '../node/extHost.protocol'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) @@ -16,28 +16,35 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private _proxy: ExtHostTerminalServiceShape; private _toDispose: IDisposable[] = []; private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {}; - private _dataListeners: { [id: number]: IDisposable } = {}; + private _terminalOnDidWriteDataListeners: { [id: number]: IDisposable } = {}; + private _terminalOnDidAcceptInputListeners: { [id: number]: IDisposable } = {}; constructor( extHostContext: IExtHostContext, @ITerminalService private terminalService: ITerminalService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._toDispose.push(terminalService.onInstanceCreated((terminalInstance) => { + 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 // to register non-extension API terminals in the extension host. - setTimeout(() => this._onTerminalOpened(terminalInstance), 100); + setTimeout(() => this._onTerminalOpened(instance), EXT_HOST_CREATION_DELAY); })); - this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance))); - this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); + this._toDispose.push(terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); + this._toDispose.push(terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request))); + this._toDispose.push(terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.id : undefined))); // Set initial ext host state this.terminalService.terminalInstances.forEach(t => { this._onTerminalOpened(t); t.processReady.then(() => this._onTerminalProcessIdReady(t)); }); + const activeInstance = this.terminalService.getActiveInstance(); + if (activeInstance) { + this._proxy.$acceptActiveTerminalChanged(activeInstance.id); + } } public dispose(): void { @@ -47,7 +54,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // when the extension host process goes down ? } - public $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean): TPromise { + public $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean): Thenable { const shellLaunchConfig: IShellLaunchConfig = { name, executable: shellPath, @@ -60,8 +67,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape return TPromise.as(this.terminalService.createTerminal(shellLaunchConfig).id); } + public $createTerminalRenderer(name: string): Thenable { + const instance = this.terminalService.createTerminalRenderer(name); + return TPromise.as(instance.id); + } + public $show(terminalId: number, preserveFocus: boolean): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { this.terminalService.setActiveInstance(terminalInstance); this.terminalService.showPanel(!preserveFocus); @@ -75,31 +87,86 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $dispose(terminalId: number): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { terminalInstance.dispose(); } } + public $terminalRendererWrite(terminalId: number, text: string): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.write(text); + } + } + + public $terminalRendererSetName(terminalId: number, name: string): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.setTitle(name, false); + } + } + + public $terminalRendererSetDimensions(terminalId: number, dimensions: ITerminalDimensions): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.setDimensions(dimensions); + } + } + + public $terminalRendererRegisterOnInputListener(terminalId: number): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (!terminalInstance) { + return; + } + + // Listener already registered + if (this._terminalOnDidAcceptInputListeners.hasOwnProperty(terminalId)) { + return; + } + + // Register + this._terminalOnDidAcceptInputListeners[terminalId] = terminalInstance.onRendererInput(data => this._onTerminalRendererInput(terminalId, data)); + terminalInstance.addDisposable(this._terminalOnDidAcceptInputListeners[terminalId]); + } + public $sendText(terminalId: number, text: string, addNewLine: boolean): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { terminalInstance.sendText(text, addNewLine); } } public $registerOnDataListener(terminalId: number): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); - if (terminalInstance) { - this._dataListeners[terminalId] = terminalInstance.onData(data => this._onTerminalData(terminalId, data)); - terminalInstance.onDisposed(instance => delete this._dataListeners[terminalId]); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (!terminalInstance) { + return; } + + // Listener already registered + if (this._terminalOnDidWriteDataListeners[terminalId]) { + return; + } + + // Register + this._terminalOnDidWriteDataListeners[terminalId] = terminalInstance.onData(data => { + this._onTerminalData(terminalId, data); + }); + terminalInstance.addDisposable(this._terminalOnDidWriteDataListeners[terminalId]); + } + + private _onActiveTerminalChanged(terminalId: number | undefined): void { + this._proxy.$acceptActiveTerminalChanged(terminalId); } private _onTerminalData(terminalId: number, data: string): void { this._proxy.$acceptTerminalProcessData(terminalId, data); } + private _onTerminalRendererInput(terminalId: number, data: string): void { + this._proxy.$acceptTerminalRendererInput(terminalId, data); + } + private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalClosed(terminalInstance.id); } @@ -112,6 +179,14 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId); } + private _onInstanceDimensionsChanged(instance: ITerminalInstance): void { + // Only send the dimensions if the terminal is a renderer only as there is no API to access + // dimensions on a plain Terminal. + if (instance.shellLaunchConfig.isRendererOnly) { + this._proxy.$acceptTerminalRendererDimensions(instance.id, instance.cols, instance.rows); + } + } + private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { this._terminalProcesses[request.proxy.terminalId] = request.proxy; const shellLaunchConfigDto: ShellLaunchConfigDto = { @@ -123,8 +198,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape }; this._proxy.$createProcess(request.proxy.terminalId, shellLaunchConfigDto, request.cols, request.rows); request.proxy.onInput(data => this._proxy.$acceptProcessInput(request.proxy.terminalId, data)); - request.proxy.onResize((cols, rows) => this._proxy.$acceptProcessResize(request.proxy.terminalId, cols, rows)); - request.proxy.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId)); + 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)); } public $sendProcessTitle(terminalId: number, title: string): void { diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index bf456a9571e..0b909bd4ba7 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -34,13 +34,14 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie if (viewer) { viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); + this._proxy.$setVisible(treeViewId, viewer.visible); } else { this.notificationService.error('No view is registered with id: ' + treeViewId); } } - $reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean }): TPromise { - return this.viewsService.openView(treeViewId) + $reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options: { select: boolean, focus: boolean }): Thenable { + return this.viewsService.openView(treeViewId, options.focus) .then(() => { const viewer = this.getTreeViewer(treeViewId); return viewer ? viewer.reveal(item, parentChain, options) : null; @@ -61,6 +62,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._register(treeViewer.onDidExpandItem(item => this._proxy.$setExpanded(treeViewId, item.handle, true))); this._register(treeViewer.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false))); this._register(treeViewer.onDidChangeSelection(items => this._proxy.$setSelection(treeViewId, items.map(({ handle }) => handle)))); + this._register(treeViewer.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible))); } private getTreeViewer(treeViewId: string): ITreeViewer { @@ -96,13 +98,13 @@ class TreeViewDataProvider implements ITreeViewDataProvider { if (treeItem && treeItem.children) { return TPromise.as(treeItem.children); } - return this._proxy.$getChildren(this.treeViewId, treeItem ? treeItem.handle : void 0) + return TPromise.wrap(this._proxy.$getChildren(this.treeViewId, treeItem ? treeItem.handle : void 0) .then(children => { return this.postGetChildren(children); }, err => { this.notificationService.error(err); return []; - }); + })); } getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] { diff --git a/src/vs/workbench/api/electron-browser/mainThreadUrls.ts b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts index 7ba2ed813b1..f2ee377a15a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadUrls.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadUrls.ts @@ -7,9 +7,9 @@ import { ExtHostContext, IExtHostContext, MainContext, MainThreadUrlsShape, ExtH import { extHostNamedCustomer } from './extHostCustomers'; import { TPromise } from 'vs/base/common/winjs.base'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionUrlHandler } from 'vs/platform/url/electron-browser/inactiveExtensionUrlHandler'; +import { IExtensionUrlHandler } from 'vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler'; class ExtensionUrlHandler implements IURLHandler { @@ -24,7 +24,7 @@ class ExtensionUrlHandler implements IURLHandler { return TPromise.as(false); } - return this.proxy.$handleExternalUri(this.handle, uri).then(() => true); + return TPromise.wrap(this.proxy.$handleExternalUri(this.handle, uri)).then(() => true); } } @@ -42,7 +42,7 @@ export class MainThreadUrls implements MainThreadUrlsShape { this.proxy = context.getProxy(ExtHostContext.ExtHostUrls); } - $registerProtocolHandler(handle: number, extensionId: string): TPromise { + $registerUriHandler(handle: number, extensionId: string): Thenable { const handler = new ExtensionUrlHandler(this.proxy, handle, extensionId); const disposable = this.urlService.registerHandler(handler); @@ -52,7 +52,7 @@ export class MainThreadUrls implements MainThreadUrlsShape { return TPromise.as(null); } - $unregisterProtocolHandler(handle: number): TPromise { + $unregisterUriHandler(handle: number): Thenable { const tuple = this.handlers.get(handle); if (!tuple) { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 9c81c36eeda..03c26a2eb02 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -2,22 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as map from 'vs/base/common/map'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { EditorViewColumn, viewColumnToEditorGroup, editorGroupToViewColumn } from 'vs/workbench/api/shared/editor'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/node/extHost.protocol'; +import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor'; import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; -import { IWebviewEditorService, WebviewInputOptions, WebviewReviver, ICreateWebViewShowOptions } from 'vs/workbench/parts/webview/electron-browser/webviewEditorService'; +import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions, WebviewReviver } from 'vs/workbench/parts/webview/electron-browser/webviewEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import * as vscode from 'vscode'; import { extHostNamedCustomer } from './extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadWebviews) @@ -39,9 +39,8 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv constructor( context: IExtHostContext, - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @ILifecycleService lifecycleService: ILifecycleService, + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, @IWebviewEditorService private readonly _webviewService: IWebviewEditorService, @IOpenerService private readonly _openerService: IOpenerService, @@ -59,11 +58,11 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv }, this, this._toDispose); } - dispose(): void { + public dispose(): void { this._toDispose = dispose(this._toDispose); } - $createWebviewPanel( + public $createWebviewPanel( handle: WebviewPanelHandle, viewType: string, title: string, @@ -77,10 +76,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); } - const webview = this._webviewService.createWebview(MainThreadWebviews.viewType, title, mainThreadShowOptions, { - ...options, - localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(URI.revive) : undefined - }, URI.revive(extensionLocation), this.createWebviewEventDelegate(handle)); + const webview = this._webviewService.createWebview(MainThreadWebviews.viewType, title, mainThreadShowOptions, reviveWebviewOptions(options), URI.revive(extensionLocation), this.createWebviewEventDelegate(handle)); webview.state = { viewType: viewType, state: undefined @@ -90,33 +86,43 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._activeWebview = handle; } - $disposeWebview(handle: WebviewPanelHandle): void { + public $disposeWebview(handle: WebviewPanelHandle): void { const webview = this.getWebview(handle); webview.dispose(); } - $setTitle(handle: WebviewPanelHandle, value: string): void { + public $setTitle(handle: WebviewPanelHandle, value: string): void { const webview = this.getWebview(handle); webview.setName(value); } - $setHtml(handle: WebviewPanelHandle, value: string): void { + public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void { + const webview = this.getWebview(handle); + webview.iconPath = reviveWebviewIcon(value); + } + + public $setHtml(handle: WebviewPanelHandle, value: string): void { const webview = this.getWebview(handle); webview.html = value; } - $reveal(handle: WebviewPanelHandle, viewColumn: EditorViewColumn | null, preserveFocus: boolean): void { + public $setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void { + const webview = this.getWebview(handle); + webview.setOptions(reviveWebviewOptions(options)); + } + + public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void { const webview = this.getWebview(handle); if (webview.isDisposed()) { return; } - const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, viewColumn)); + const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)); - this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.activeGroup, preserveFocus); + this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.activeGroup, showOptions.preserveFocus); } - async $postMessage(handle: WebviewPanelHandle, message: any): TPromise { + public $postMessage(handle: WebviewPanelHandle, message: any): Thenable { const webview = this.getWebview(handle); const editors = this._editorService.visibleControls .filter(e => e instanceof WebviewEditor) @@ -127,18 +133,18 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv editor.sendMessage(message); } - return (editors.length > 0); + return TPromise.as(editors.length > 0); } - $registerSerializer(viewType: string): void { + public $registerSerializer(viewType: string): void { this._revivers.add(viewType); } - $unregisterSerializer(viewType: string): void { + public $unregisterSerializer(viewType: string): void { this._revivers.delete(viewType); } - reviveWebview(webview: WebviewEditorInput): TPromise { + public reviveWebview(webview: WebviewEditorInput): TPromise { const viewType = webview.state.viewType; return this._extensionService.activateByEvent(`onWebviewPanel:${viewType}`).then(() => { const handle = 'revival-' + MainThreadWebviews.revivalPool++; @@ -161,7 +167,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv }); } - canRevive(webview: WebviewEditorInput): boolean { + public canRevive(webview: WebviewEditorInput): boolean { if (webview.isDisposed() || !webview.state) { return false; } @@ -177,7 +183,6 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv }); return TPromise.as(false); // Don't veto shutdown - } private createWebviewEventDelegate(handle: WebviewPanelHandle) { @@ -185,21 +190,16 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv onDidClickLink: uri => this.onDidClickLink(handle, uri), onMessage: message => this._proxy.$onMessage(handle, message), onDispose: () => { + const cleanUp = () => { + this._webviews.delete(handle); + }; this._proxy.$onDidDisposeWebviewPanel(handle).then( - () => this._webviews.delete(handle), - () => this._webviews.delete(handle)); + cleanUp, + cleanUp); } }; } - private getWebview(handle: WebviewPanelHandle): WebviewEditorInput { - const webview = this._webviews.get(handle); - if (!webview) { - throw new Error('Unknown webview handle:' + handle); - } - return webview; - } - private onActiveEditorChanged() { const activeEditor = this._editorService.activeControl; let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined; @@ -215,7 +215,11 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv if (newActiveWebview && newActiveWebview.handle === this._activeWebview) { // Webview itself unchanged but position may have changed - this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, true, editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group)); + this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, { + active: true, + visible: true, + position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group) + }); return; } @@ -223,13 +227,21 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv if (typeof this._activeWebview !== 'undefined') { const oldActiveWebview = this._webviews.get(this._activeWebview); if (oldActiveWebview) { - this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, false, editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group)); + this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, { + active: false, + visible: this._editorService.visibleControls.some(editor => editor.input && editor.input.matches(oldActiveWebview)), + position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group), + }); } } // Then for newly active if (newActiveWebview) { - this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, true, editorGroupToViewColumn(this._editorGroupService, activeEditor.group)); + this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, { + active: true, + visible: true, + position: editorGroupToViewColumn(this._editorGroupService, activeEditor.group) + }); this._activeWebview = newActiveWebview.handle; } else { this._activeWebview = undefined; @@ -237,22 +249,23 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv } private onVisibleEditorsChanged(): void { - for (const workbenchEditor of this._editorService.visibleControls) { - if (!workbenchEditor.input) { - return; - } + this._webviews.forEach((input, handle) => { + for (const workbenchEditor of this._editorService.visibleControls) { + if (workbenchEditor.input && workbenchEditor.input.matches(input)) { + const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group); - this._webviews.forEach((input, handle) => { - const inputPosition = editorGroupToViewColumn(this._editorGroupService, input.group); - const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group); - - if (workbenchEditor.input.matches(input) && inputPosition !== editorPosition) { input.updateGroup(workbenchEditor.group.id); - this._proxy.$onDidChangeWebviewPanelViewState(handle, handle === this._activeWebview, editorPosition); + this._proxy.$onDidChangeWebviewPanelViewState(handle, { + active: handle === this._activeWebview, + visible: true, + position: editorPosition + }); + break; } - }); - } + } + }); } + private onDidClickLink(handle: WebviewPanelHandle, link: URI): void { if (!link) { return; @@ -265,6 +278,14 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv } } + private getWebview(handle: WebviewPanelHandle): WebviewEditorInput { + const webview = this._webviews.get(handle); + if (!webview) { + throw new Error('Unknown webview handle:' + handle); + } + return webview; + } + private static getDeserializationFailedContents(viewType: string) { return ` @@ -276,4 +297,24 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv ${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)} `; } +} + +function reviveWebviewOptions(options: WebviewInputOptions): WebviewInputOptions { + return { + ...options, + localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(URI.revive) : undefined + }; +} + +function reviveWebviewIcon( + value: { light: UriComponents, dark: UriComponents } | undefined +): { light: URI, dark: URI } | undefined { + if (!value) { + return undefined; + } + + return { + light: URI.revive(value.light), + dark: URI.revive(value.dark) + }; } \ No newline at end of file diff --git a/src/vs/workbench/api/electron-browser/mainThreadWindow.ts b/src/vs/workbench/api/electron-browser/mainThreadWindow.ts index 78a3143fd09..c072e83a5f9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWindow.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { MainThreadWindowShape, ExtHostWindowShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -27,7 +26,7 @@ export class MainThreadWindow implements MainThreadWindowShape { (this.proxy.$onDidChangeWindowFocus, this.proxy, this.disposables); } - $getWindowVisibility(): TPromise { + $getWindowVisibility(): Thenable { return this.windowService.isFocused(); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index c85997bc5d4..29a81a2b32d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -5,24 +5,32 @@ 'use strict'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { ISearchService, QueryType, ISearchQuery, IFolderQuery, ISearchConfiguration } from 'vs/platform/search/common/search'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { localize } from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IFolderQuery, IPatternInfo, IQueryOptions, ISearchConfiguration, ISearchProgressItem, ISearchQuery, ISearchService, QueryType } from 'vs/platform/search/common/search'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape } from '../node/extHost.protocol'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { private readonly _toDispose: IDisposable[] = []; - private readonly _activeSearches: { [id: number]: TPromise } = Object.create(null); + private readonly _activeCancelTokens: { [id: number]: CancellationTokenSource } = Object.create(null); private readonly _proxy: ExtHostWorkspaceShape; constructor( @@ -32,7 +40,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ITextFileService private readonly _textFileService: ITextFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, - @IStatusbarService private readonly _statusbarService: IStatusbarService + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); @@ -42,9 +52,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { dispose(): void { dispose(this._toDispose); - for (let requestId in this._activeSearches) { - const search = this._activeSearches[requestId]; - search.cancel(); + for (let requestId in this._activeCancelTokens) { + const tokenSource = this._activeCancelTokens[requestId]; + tokenSource.cancel(); } } @@ -92,12 +102,18 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } private _onDidChangeWorkspace(): void { - this._proxy.$acceptWorkspaceData(this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace()); + const workspace = this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace(); + this._proxy.$acceptWorkspaceData(workspace ? { + configuration: workspace.configuration, + folders: workspace.folders, + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace) + } : null); } // --- search --- - $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable { + $startFileSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, token: CancellationToken): Thenable { const workspace = this._contextService.getWorkspace(); if (!workspace.folders.length) { return undefined; @@ -128,15 +144,21 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { folderQueries, type: QueryType.File, maxResults, - includePattern: { [typeof includePattern === 'string' ? includePattern : undefined]: true }, - excludePattern: { [typeof excludePatternOrDisregardExcludes === 'string' ? excludePatternOrDisregardExcludes : undefined]: true }, disregardExcludeSettings: excludePatternOrDisregardExcludes === false, useRipgrep, ignoreSymlinks }; + if (typeof includePattern === 'string') { + query.includePattern = { [includePattern]: true }; + } + + if (typeof excludePatternOrDisregardExcludes === 'string') { + query.excludePattern = { [excludePatternOrDisregardExcludes]: true }; + } + this._searchService.extendQuery(query); - const search = this._searchService.search(query).then(result => { + return this._searchService.search(query, token).then(result => { return result.results.map(m => m.resource); }, err => { if (!isPromiseCanceledError(err)) { @@ -144,22 +166,55 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } return undefined; }); + } - this._activeSearches[requestId] = search; - const onDone = () => delete this._activeSearches[requestId]; - search.done(onDone, onDone); + $startTextSearch(pattern: IPatternInfo, options: IQueryOptions, requestId: number, token: CancellationToken): Thenable { + const workspace = this._contextService.getWorkspace(); + const folders = workspace.folders.map(folder => folder.uri); + + const queryBuilder = this._instantiationService.createInstance(QueryBuilder); + const query = queryBuilder.text(pattern, folders, options); + + const onProgress = (p: ISearchProgressItem) => { + if (p.matches) { + this._proxy.$handleTextSearchResult(p, requestId); + } + }; + + const search = this._searchService.search(query, token, onProgress).then( + () => { + return null; + }, + err => { + if (!isPromiseCanceledError(err)) { + return TPromise.wrapError(err); + } + + return undefined; + }); return search; } - $cancelSearch(requestId: number): Thenable { - const search = this._activeSearches[requestId]; - if (search) { - delete this._activeSearches[requestId]; - search.cancel(); - return TPromise.as(true); - } - return undefined; + $checkExists(includes: string[], token: CancellationToken): Thenable { + const queryBuilder = this._instantiationService.createInstance(QueryBuilder); + const folders = this._contextService.getWorkspace().folders.map(folder => folder.uri); + const query = queryBuilder.file(folders, { + includePattern: includes.join(', '), + exists: true + }); + + return this._searchService.search(query, token).then( + result => { + return result.limitHit; + }, + err => { + if (!isPromiseCanceledError(err)) { + return TPromise.wrapError(err); + } + + return undefined; + }); } // --- save & edit resources --- @@ -170,3 +225,19 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { }); } } + +CommandsRegistry.registerCommand('_workbench.enterWorkspace', async function (accessor: ServicesAccessor, workspace: URI, disableExtensions: string[]) { + const workspaceEditingService = accessor.get(IWorkspaceEditingService); + const extensionService = accessor.get(IExtensionService); + const windowService = accessor.get(IWindowService); + + if (disableExtensions && disableExtensions.length) { + const runningExtensions = await extensionService.getExtensions(); + // If requested extension to disable is running, then reload window with given workspace + if (disableExtensions && runningExtensions.some(runningExtension => disableExtensions.some(id => areSameExtensions({ id }, { id: runningExtension.id })))) { + return windowService.openWindow([URI.file(workspace.fsPath)], { args: { _: [], 'disable-extension': disableExtensions } }); + } + } + + return workspaceEditingService.enterWorkspace(workspace.fsPath); +}); diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index 5be2fe4cba7..03dfaf4a9e1 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +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'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -47,8 +49,14 @@ export class OpenFolderAPICommand { if (!uri) { return executor.executeCommand('_files.pickFolderAndOpen', forceNewWindow); } + let correctedUri = isMalformedFileUri(uri); + if (correctedUri) { + // workaround for #55916 and #55891, will be removed in 1.28 + console.warn(`'vscode.openFolder' command invoked with an invalid URI (file:// scheme missing): '${uri}'. Converted to a 'file://' URI: ${correctedUri}`); + uri = correctedUri; + } - return executor.executeCommand('_files.windowOpen', [uri.fsPath], forceNewWindow); + return executor.executeCommand('_files.windowOpen', [uri], forceNewWindow); } } CommandsRegistry.registerCommand(OpenFolderAPICommand.ID, adjustHandler(OpenFolderAPICommand.execute)); @@ -98,3 +106,11 @@ export class RemoveFromRecentlyOpenedAPICommand { } } CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); + +export class SetEditorLayoutAPICommand { + public static ID = 'vscode.setEditorLayout'; + public static execute(executor: ICommandsExecutor, layout: EditorGroupLayout): Thenable { + return executor.executeCommand('layoutEditorGroups', layout); + } +} +CommandsRegistry.registerCommand(SetEditorLayoutAPICommand.ID, adjustHandler(SetEditorLayoutAPICommand.execute)); \ 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 74de308b1c0..d445d204876 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -37,7 +37,7 @@ import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService'; import { ExtHostWindow } from 'vs/workbench/api/node/extHostWindow'; import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; @@ -46,7 +46,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as vscode from 'vscode'; import * as paths from 'vs/base/common/paths'; import * as files from 'vs/platform/files/common/files'; -import { MainContext, ExtHostContext, IInitData, IExtHostContext } from './extHost.protocol'; +import { MainContext, ExtHostContext, IInitData, IMainContext } from './extHost.protocol'; import * as languageConfiguration from 'vs/editor/common/modes/languageConfiguration'; import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; @@ -61,6 +61,7 @@ import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; import { ExtHostComments } from './extHostComments'; import { ExtHostSearch } from './extHostSearch'; import { ExtHostUrls } from './extHostUrls'; +import { localize } from 'vs/nls'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -80,7 +81,7 @@ function proposedApiFunction(extension: IExtensionDescription, fn: T): T { if (extension.enableProposedApi) { return fn; } else { - return throwProposedApiError; + return throwProposedApiError.bind(null, extension); } } @@ -89,7 +90,7 @@ function proposedApiFunction(extension: IExtensionDescription, fn: T): T { */ export function createApiFactory( initData: IInitData, - rpcProtocol: IExtHostContext, + rpcProtocol: IMainContext, extHostWorkspace: ExtHostWorkspace, extHostConfiguration: ExtHostConfiguration, extensionService: ExtHostExtensionService, @@ -110,16 +111,16 @@ export function createApiFactory( const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors))); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(rpcProtocol, extHostHeapService, extHostLogService)); - const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands)); + const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); - const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService, extHostDocumentsAndEditors, extHostConfiguration)); rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol)); - const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, schemeTransformer, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); + const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, schemeTransformer, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics, extHostLogService)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); - const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService()); + const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostDocumentsAndEditors)); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostLogService)); + const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService, extHostDocumentsAndEditors, extHostConfiguration, extHostTerminalService)); const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService)); const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol, schemeTransformer)); const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace, extHostDocumentsAndEditors, extHostConfiguration)); @@ -136,9 +137,12 @@ export function createApiFactory( const extHostMessageService = new ExtHostMessageService(rpcProtocol); const extHostDialogs = new ExtHostDialogs(rpcProtocol); const extHostStatusBar = new ExtHostStatusBar(rpcProtocol); - const extHostOutputService = new ExtHostOutputService(rpcProtocol); + const extHostOutputService = new ExtHostOutputService(initData.logsLocation, rpcProtocol); const extHostLanguages = new ExtHostLanguages(rpcProtocol); + // Register an output channel for exthost log + extHostOutputService.createOutputChannelFromLogFile(localize('extensionsLog', "Extension Host"), extHostLogService.logFile); + // Register API-ish commands ExtHostApiCommands.register(extHostCommands); @@ -226,8 +230,15 @@ export function createApiFactory( get sessionId() { return initData.telemetryInfo.sessionId; }, get language() { return platform.language; }, get appName() { return product.nameLong; }, - get appRoot() { return initData.environment.appRoot; }, - get logLevel() { return extHostLogService.getLevel(); } + get appRoot() { return initData.environment.appRoot.fsPath; }, + get logLevel() { + checkProposedApiEnabled(extension); + return extHostLogService.getLevel(); + }, + get onDidChangeLogLevel() { + checkProposedApiEnabled(extension); + return extHostLogService.onDidChangeLogLevel; + } }); // namespace: extensions @@ -255,14 +266,18 @@ export function createApiFactory( getDiagnostics: (resource?: vscode.Uri) => { return extHostDiagnostics.getDiagnostics(resource); }, - getLanguages(): TPromise { + getLanguages(): Thenable { return extHostLanguages.getLanguages(); }, + changeLanguage(document: vscode.TextDocument, languageId: string): Thenable { + checkProposedApiEnabled(extension); + return extHostLanguages.changeLanguage(document.uri, languageId); + }, match(selector: vscode.DocumentSelector, document: vscode.TextDocument): number { return score(typeConverters.LanguageSelector.from(selector), document.uri, document.languageId, true); }, registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { - return extHostLanguageFeatures.registerCodeActionProvider(checkSelector(selector), provider, metadata); + return extHostLanguageFeatures.registerCodeActionProvider(checkSelector(selector), provider, extension, metadata); }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { return extHostLanguageFeatures.registerCodeLensProvider(checkSelector(selector), provider); @@ -289,7 +304,7 @@ export function createApiFactory( return extHostLanguageFeatures.registerRenameProvider(checkSelector(selector), provider); }, registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentSymbolProvider(checkSelector(selector), provider, extension.id); + return extHostLanguageFeatures.registerDocumentSymbolProvider(checkSelector(selector), provider, extension); }, registerWorkspaceSymbolProvider(provider: vscode.WorkspaceSymbolProvider): vscode.Disposable { return extHostLanguageFeatures.registerWorkspaceSymbolProvider(provider); @@ -331,10 +346,13 @@ export function createApiFactory( get visibleTextEditors() { return extHostEditors.getVisibleTextEditors(); }, - get terminals() { - return proposedApiFunction(extension, extHostTerminalService.terminals); + get activeTerminal() { + return proposedApiFunction(extension, extHostTerminalService.activeTerminal); }, - showTextDocument(documentOrUri: vscode.TextDocument | vscode.Uri, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): TPromise { + get terminals() { + return extHostTerminalService.terminals; + }, + showTextDocument(documentOrUri: vscode.TextDocument | vscode.Uri, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): Thenable { let documentPromise: TPromise; if (URI.isUri(documentOrUri)) { documentPromise = TPromise.wrap(workspace.openTextDocument(documentOrUri)); @@ -369,8 +387,11 @@ export function createApiFactory( onDidCloseTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidCloseTerminal(listener, thisArg, disposables); }, - onDidOpenTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + onDidOpenTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidOpenTerminal(listener, thisArg, disposables); + }, + onDidChangeActiveTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostTerminalService.onDidChangeActiveTerminal(listener, thisArg, disposables); }), get state() { return extHostWindow.state; @@ -388,13 +409,13 @@ export function createApiFactory( return extHostMessageService.showMessage(extension, Severity.Error, message, first, rest); }, showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken): any { - return extHostQuickOpen.showQuickPick(undefined, items, options, token); + return extHostQuickOpen.showQuickPick(items, options, token); }, showWorkspaceFolderPick(options: vscode.WorkspaceFolderPickOptions) { return extHostQuickOpen.showWorkspaceFolderPick(options); }, showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { - return extHostQuickOpen.showInput(undefined, options, token); + return extHostQuickOpen.showInput(options, token); }, showOpenDialog(options) { return extHostDialogs.showOpenDialog(options); @@ -415,11 +436,11 @@ export function createApiFactory( withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { return extHostProgress.withProgress(extension, options, task); }, - createOutputChannel(name: string): vscode.OutputChannel { - return extHostOutputService.createOutputChannel(name); + createOutputChannel(name: string, push?: boolean): vscode.OutputChannel { + return extHostOutputService.createOutputChannel(name, push); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebview(viewType, title, showOptions, options, extension.extensionLocation); + return extHostWebviews.createWebview(extension.extensionLocation, viewType, title, showOptions, options); }, createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { if (typeof nameOrOptions === 'object') { @@ -427,12 +448,18 @@ export function createApiFactory( } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, + createTerminalRenderer: proposedApiFunction(extension, (name: string) => { + return extHostTerminalService.createTerminalRenderer(name); + }), registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider); }, createTreeView(viewId: string, options: { treeDataProvider: vscode.TreeDataProvider }): vscode.TreeView { return extHostTreeViews.createTreeView(viewId, options); }, + registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { + return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); + }, // proposed API sampleFunction: proposedApiFunction(extension, () => { return extHostMessageService.showMessage(extension, Severity.Info, 'Hello Proposed Api!', {}, []); @@ -440,12 +467,15 @@ export function createApiFactory( registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); }), - registerWebviewPanelSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); - }), - registerProtocolHandler: proposedApiFunction(extension, (handler: vscode.ProtocolHandler) => { - return extHostUrls.registerProtocolHandler(extension.id, handler); - }) + registerUriHandler(handler: vscode.UriHandler) { + return extHostUrls.registerUriHandler(extension.id, handler); + }, + createQuickPick(): vscode.QuickPick { + return extHostQuickOpen.createQuickPick(extension.id); + }, + createInputBox(): vscode.InputBox { + return extHostQuickOpen.createInputBox(extension.id); + }, }; // namespace: workspace @@ -463,7 +493,7 @@ export function createApiFactory( return extHostWorkspace.getWorkspaceFolders(); }, get name() { - return extHostWorkspace.workspace ? extHostWorkspace.workspace.name : undefined; + return extHostWorkspace.name; }, set name(value) { throw errors.readonly(); @@ -480,10 +510,25 @@ export function createApiFactory( findFiles: (include, exclude, maxResults?, token?) => { return extHostWorkspace.findFiles(typeConverters.GlobPattern.from(include), typeConverters.GlobPattern.from(exclude), maxResults, extension.id, token); }, + findTextInFiles: (query: vscode.TextSearchQuery, optionsOrCallback, callbackOrToken?, token?: vscode.CancellationToken) => { + let options: vscode.FindTextInFilesOptions; + let callback: (result: vscode.TextSearchResult) => void; + + if (typeof optionsOrCallback === 'object') { + options = optionsOrCallback; + callback = callbackOrToken; + } else { + options = {}; + callback = optionsOrCallback; + token = callbackOrToken; + } + + return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.id, token); + }, saveAll: (includeUntitled?) => { return extHostWorkspace.saveAll(includeUntitled); }, - applyEdit(edit: vscode.WorkspaceEdit): TPromise { + applyEdit(edit: vscode.WorkspaceEdit): Thenable { return extHostEditors.applyWorkspaceEdit(edit); }, createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => { @@ -496,7 +541,7 @@ export function createApiFactory( throw errors.readonly(); }, openTextDocument(uriOrFileNameOrOptions?: vscode.Uri | string | { language?: string; content?: string; }) { - let uriPromise: TPromise; + let uriPromise: Thenable; let options = uriOrFileNameOrOptions as { language?: string; content?: string; }; if (typeof uriOrFileNameOrOptions === 'string') { @@ -547,8 +592,18 @@ export function createApiFactory( registerFileSystemProvider(scheme, provider, options) { return extHostFileSystem.registerFileSystemProvider(scheme, provider, options); }, - registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => { - return extHostSearch.registerSearchProvider(scheme, provider); + registerFileSearchProvider: proposedApiFunction(extension, (scheme, provider) => { + return extHostSearch.registerFileSearchProvider(scheme, provider); + }), + registerSearchProvider: proposedApiFunction(extension, () => { + // Temp for live share in Insiders + return { dispose: () => { } }; + }), + registerTextSearchProvider: proposedApiFunction(extension, (scheme, provider) => { + return extHostSearch.registerTextSearchProvider(scheme, provider); + }), + registerFileIndexProvider: proposedApiFunction(extension, (scheme, provider) => { + return extHostSearch.registerFileIndexProvider(scheme, provider); }), registerDocumentCommentProvider: proposedApiFunction(extension, (provider: vscode.DocumentCommentProvider) => { return exthostCommentProviders.registerDocumentCommentProvider(provider); @@ -556,8 +611,11 @@ export function createApiFactory( registerWorkspaceCommentProvider: proposedApiFunction(extension, (provider: vscode.WorkspaceCommentProvider) => { return exthostCommentProviders.registerWorkspaceCommentProvider(provider); }), - onDidRenameResource: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { - return extHostDocuments.onDidRenameResource(listener, thisArg, disposables); + onDidRenameFile: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables); + }), + onWillRenameFile: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); }) }; @@ -642,39 +700,48 @@ export function createApiFactory( version: pkg.version, // namespaces commands, + debug, env, extensions, languages, + scm, + tasks, window, workspace, - scm, - debug, - tasks, // types Breakpoint: extHostTypes.Breakpoint, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, CodeActionKind: extHostTypes.CodeActionKind, + CodeActionTrigger: extHostTypes.CodeActionTrigger, CodeLens: extHostTypes.CodeLens, Color: extHostTypes.Color, - ColorPresentation: extHostTypes.ColorPresentation, ColorInformation: extHostTypes.ColorInformation, - CodeActionTrigger: extHostTypes.CodeActionTrigger, - EndOfLine: extHostTypes.EndOfLine, + ColorPresentation: extHostTypes.ColorPresentation, + CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionList: extHostTypes.CompletionList, CompletionTriggerKind: extHostTypes.CompletionTriggerKind, + ConfigurationTarget: extHostTypes.ConfigurationTarget, DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable, + DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, Diagnostic: extHostTypes.Diagnostic, DiagnosticRelatedInformation: extHostTypes.DiagnosticRelatedInformation, - DiagnosticTag: extHostTypes.DiagnosticTag, DiagnosticSeverity: extHostTypes.DiagnosticSeverity, + DiagnosticTag: extHostTypes.DiagnosticTag, Disposable: extHostTypes.Disposable, DocumentHighlight: extHostTypes.DocumentHighlight, DocumentHighlightKind: extHostTypes.DocumentHighlightKind, DocumentLink: extHostTypes.DocumentLink, + DocumentSymbol: extHostTypes.DocumentSymbol, + EndOfLine: extHostTypes.EndOfLine, EventEmitter: Emitter, + FileChangeType: extHostTypes.FileChangeType, + FileSystemError: extHostTypes.FileSystemError, + FileType: files.FileType, + FoldingRange: extHostTypes.FoldingRange, + FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, @@ -684,60 +751,58 @@ export function createApiFactory( OverviewRulerLane: OverviewRulerLane, ParameterInformation: extHostTypes.ParameterInformation, Position: extHostTypes.Position, + ProcessExecution: extHostTypes.ProcessExecution, + ProgressLocation: extHostTypes.ProgressLocation, + QuickInputButtons: extHostTypes.QuickInputButtons, Range: extHostTypes.Range, + RelativePattern: extHostTypes.RelativePattern, Selection: extHostTypes.Selection, + ShellExecution: extHostTypes.ShellExecution, + ShellQuoting: extHostTypes.ShellQuoting, + SignatureHelpTriggerReason: extHostTypes.SignatureHelpTriggerReason, SignatureHelp: extHostTypes.SignatureHelp, SignatureInformation: extHostTypes.SignatureInformation, SnippetString: extHostTypes.SnippetString, SourceBreakpoint: extHostTypes.SourceBreakpoint, + SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, StatusBarAlignment: extHostTypes.StatusBarAlignment, SymbolInformation: extHostTypes.SymbolInformation, - SymbolInformation2: class extends extHostTypes.SymbolInformation2 { - constructor(name, kind, containerName, location) { - checkProposedApiEnabled(extension); - super(name, kind, containerName, location); - } - }, SymbolKind: extHostTypes.SymbolKind, - SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, + Task: extHostTypes.Task, + TaskGroup: extHostTypes.TaskGroup, + TaskPanelKind: extHostTypes.TaskPanelKind, + TaskRevealKind: extHostTypes.TaskRevealKind, + TaskScope: extHostTypes.TaskScope, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, TextEditorCursorStyle: TextEditorCursorStyle, TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle, TextEditorRevealType: extHostTypes.TextEditorRevealType, TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind, - DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, + ThemeColor: extHostTypes.ThemeColor, + ThemeIcon: extHostTypes.ThemeIcon, + TreeItem: extHostTypes.TreeItem, + TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState, Uri: URI, ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, - ProgressLocation: extHostTypes.ProgressLocation, - TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState, - ThemeIcon: extHostTypes.ThemeIcon, - TreeItem: extHostTypes.TreeItem, - ThemeColor: extHostTypes.ThemeColor, // functions - TaskRevealKind: extHostTypes.TaskRevealKind, - TaskPanelKind: extHostTypes.TaskPanelKind, - TaskGroup: extHostTypes.TaskGroup, - ProcessExecution: extHostTypes.ProcessExecution, - ShellExecution: extHostTypes.ShellExecution, - ShellQuoting: extHostTypes.ShellQuoting, - TaskScope: extHostTypes.TaskScope, - Task: extHostTypes.Task, - ConfigurationTarget: extHostTypes.ConfigurationTarget, - RelativePattern: extHostTypes.RelativePattern, - - FileChangeType: extHostTypes.FileChangeType, - FileType: files.FileType, - FileSystemError: extHostTypes.FileSystemError, - FoldingRange: extHostTypes.FoldingRange, - FoldingRangeKind: extHostTypes.FoldingRangeKind, - - CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState }; }; } +/** + * Returns the original fs path (using the original casing for the drive letter) + */ +export function originalFSPath(uri: URI): string { + const result = uri.fsPath; + if (/^[a-zA-Z]:/.test(result) && uri.path.charAt(1).toLowerCase() === result.charAt(0)) { + // Restore original drive letter casing + return uri.path.charAt(1) + result.substr(1); + } + return result; +} + class Extension implements vscode.Extension { private _extensionService: ExtHostExtensionService; @@ -749,7 +814,7 @@ class Extension implements vscode.Extension { constructor(extensionService: ExtHostExtensionService, description: IExtensionDescription) { this._extensionService = extensionService; this.id = description.id; - this.extensionPath = paths.normalize(description.extensionLocation.fsPath, true); + this.extensionPath = paths.normalize(originalFSPath(description.extensionLocation), true); this.packageJSON = description; } @@ -796,7 +861,9 @@ function defineAPI(factory: IExtensionApiFactory, extensionPaths: TernarySearchT // fall back to a default implementation if (!defaultApiImpl) { - console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}`); + let extensionPathsPretty = ''; + extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.id}\n`); + console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}. These are the extension path mappings: \n${extensionPathsPretty}`); defaultApiImpl = factory(nullExtensionDescription); } return defaultApiImpl; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 3f9aa8b3f94..002f815ac7b 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -4,58 +4,50 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { createMainContextProxyIdentifier as createMainId, createExtHostContextProxyIdentifier as createExtId, ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; - -import * as vscode from 'vscode'; - -import URI, { UriComponents } from 'vs/base/common/uri'; +import { SerializedError } from 'vs/base/common/errors'; +import { IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; -import { TPromise } from 'vs/base/common/winjs.base'; - -import { IMarkerData } from 'vs/platform/markers/common/markers'; -import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; - -import * as editorCommon from 'vs/editor/common/editorCommon'; -import * as modes from 'vs/editor/common/modes'; - -import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; -import { IConfig, IAdapterExecutable, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; - -import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; -import { EndOfLine, TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; - - -import { TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; -import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; - -import { ITreeItem } from 'vs/workbench/common/views'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { SerializedError } from 'vs/base/common/errors'; -import { IStat, FileChangeType, IWatchOptions, FileSystemProviderCapabilities, FileWriteOptions, FileType, FileOverwriteOptions } from 'vs/platform/files/common/files'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +import * as editorCommon from 'vs/editor/common/editorCommon'; import { ISingleEditOperation } from 'vs/editor/common/model'; -import { IPatternInfo, IRawSearchQuery, IRawFileMatch2, ISearchCompleteStats } from 'vs/platform/search/common/search'; +import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; +import * as modes from 'vs/editor/common/modes'; +import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +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 { LabelRules } from 'vs/platform/label/common/label'; import { LogLevel } from 'vs/platform/log/common/log'; -import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO } from 'vs/workbench/api/shared/tasks'; +import { IMarkerData } from 'vs/platform/markers/common/markers'; +import { IPickOptions, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IPatternInfo, IQueryOptions, IRawFileMatch2, IRawSearchQuery, ISearchCompleteStats } from 'vs/platform/search/common/search'; +import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; +import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; +import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { EndOfLine, IFileOperationOptions, TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; +import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; +import { TaskDTO, TaskExecutionDTO, TaskFilterDTO, TaskHandleDTO, TaskProcessEndedDTO, TaskProcessStartedDTO, TaskSystemInfoDTO } from 'vs/workbench/api/shared/tasks'; +import { ITreeItem } from 'vs/workbench/common/views'; +import { IAdapterExecutable, IConfig, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; +import { ITerminalDimensions } from 'vs/workbench/parts/terminal/common/terminal'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol, ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; +import { IProgressOptions, IProgressStep } from 'vs/workbench/services/progress/common/progress'; +import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import * as vscode from 'vscode'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; - appRoot: string; - appSettingsHome: string; - disableExtensions: boolean; - extensionDevelopmentPath: string; + appRoot: URI; + appSettingsHome: URI; + extensionDevelopmentLocationURI: URI; extensionTestsPath: string; } @@ -73,9 +65,8 @@ export interface IInitData { extensions: IExtensionDescription[]; configuration: IConfigurationInitData; telemetryInfo: ITelemetryInfo; - windowId: number; logLevel: LogLevel; - logsPath: string; + logsLocation: URI; } export interface IConfigurationInitData extends IConfigurationData { @@ -111,8 +102,8 @@ export interface MainThreadCommentsShape extends IDisposable { } export interface MainThreadConfigurationShape extends IDisposable { - $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any, resource: UriComponents): TPromise; - $removeConfigurationOption(target: ConfigurationTarget, key: string, resource: UriComponents): TPromise; + $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any, resource: UriComponents): Thenable; + $removeConfigurationOption(target: ConfigurationTarget, key: string, resource: UriComponents): Thenable; } export interface MainThreadDiagnosticsShape extends IDisposable { @@ -153,9 +144,9 @@ export interface MainThreadDocumentContentProvidersShape extends IDisposable { } export interface MainThreadDocumentsShape extends IDisposable { - $tryCreateDocument(options?: { language?: string; content?: string; }): TPromise; - $tryOpenDocument(uri: UriComponents): TPromise; - $trySaveDocument(uri: UriComponents): TPromise; + $tryCreateDocument(options?: { language?: string; content?: string; }): Thenable; + $tryOpenDocument(uri: UriComponents): Thenable; + $trySaveDocument(uri: UriComponents): Thenable; } export interface ITextEditorConfigurationUpdate { @@ -196,26 +187,26 @@ export interface ITextDocumentShowOptions { } export interface MainThreadTextEditorsShape extends IDisposable { - $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): TPromise; + $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Thenable; $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; $removeTextEditorDecorationType(key: string): void; - $tryShowEditor(id: string, position: EditorViewColumn): TPromise; - $tryHideEditor(id: string): TPromise; - $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise; - $trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): TPromise; - $trySetDecorationsFast(id: string, key: string, ranges: number[]): TPromise; - $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise; - $trySetSelections(id: string, selections: ISelection[]): TPromise; - $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): TPromise; - $tryApplyWorkspaceEdit(workspaceEditDto: WorkspaceEditDto): TPromise; - $tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): TPromise; - $getDiffInformation(id: string): TPromise; + $tryShowEditor(id: string, position: EditorViewColumn): Thenable; + $tryHideEditor(id: string): Thenable; + $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Thenable; + $trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): Thenable; + $trySetDecorationsFast(id: string, key: string, ranges: number[]): Thenable; + $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Thenable; + $trySetSelections(id: string, selections: ISelection[]): Thenable; + $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Thenable; + $tryApplyWorkspaceEdit(workspaceEditDto: WorkspaceEditDto): Thenable; + $tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): Thenable; + $getDiffInformation(id: string): Thenable; } export interface MainThreadTreeViewsShape extends IDisposable { $registerTreeViewDataProvider(treeViewId: string): void; - $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): TPromise; - $reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean }): TPromise; + $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Thenable; + $reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: { select: boolean, focus: boolean }): Thenable; } export interface MainThreadErrorsShape extends IDisposable { @@ -296,7 +287,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { } export interface MainThreadLanguagesShape extends IDisposable { - $getLanguages(): TPromise; + $getLanguages(): Thenable; + $changeLanguage(resource: UriComponents, languageId: string): Thenable; } export interface MainThreadMessageOptions { @@ -309,11 +301,12 @@ export interface MainThreadMessageServiceShape extends IDisposable { } export interface MainThreadOutputServiceShape extends IDisposable { - $append(channelId: string, label: string, value: string): TPromise; - $clear(channelId: string, label: string): TPromise; - $dispose(channelId: string, label: string): TPromise; - $reveal(channelId: string, label: string, preserveFocus: boolean): TPromise; - $close(channelId: string): TPromise; + $register(label: string, log: boolean, file?: UriComponents): Thenable; + $append(channelId: string, value: string): Thenable; + $clear(channelId: string): Thenable; + $reveal(channelId: string, preserveFocus: boolean): Thenable; + $close(channelId: string): Thenable; + $dispose(channelId: string): Thenable; } export interface MainThreadProgressShape extends IDisposable { @@ -324,28 +317,99 @@ export interface MainThreadProgressShape extends IDisposable { } export interface MainThreadTerminalServiceShape extends IDisposable { - $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean): TPromise; + $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean): Thenable; + $createTerminalRenderer(name: string): Thenable; $dispose(terminalId: number): void; $hide(terminalId: number): void; $sendText(terminalId: number, text: string, addNewLine: boolean): void; $show(terminalId: number, preserveFocus: boolean): void; $registerOnDataListener(terminalId: number): void; + // Process $sendProcessTitle(terminalId: number, title: string): void; $sendProcessData(terminalId: number, data: string): void; $sendProcessPid(terminalId: number, pid: number): void; $sendProcessExit(terminalId: number, exitCode: number): void; + + // Renderer + $terminalRendererSetName(terminalId: number, name: string): void; + $terminalRendererSetDimensions(terminalId: number, dimensions: ITerminalDimensions): void; + $terminalRendererWrite(terminalId: number, text: string): void; + $terminalRendererRegisterOnInputListener(terminalId: number): void; } -export interface MyQuickPickItems extends IPickOpenEntry { +export interface TransferQuickPickItems extends IQuickPickItem { handle: number; } + +export interface TransferQuickInputButton extends IQuickInputButton { + handle: number; +} + +export type TransferQuickInput = TransferQuickPick | TransferInputBox; + +export interface BaseTransferQuickInput { + + id: number; + + type?: 'quickPick' | 'inputBox'; + + enabled?: boolean; + + busy?: boolean; + + visible?: boolean; +} + +export interface TransferQuickPick extends BaseTransferQuickInput { + + type?: 'quickPick'; + + value?: string; + + placeholder?: string; + + buttons?: TransferQuickInputButton[]; + + items?: TransferQuickPickItems[]; + + activeItems?: number[]; + + selectedItems?: number[]; + + canSelectMany?: boolean; + + ignoreFocusOut?: boolean; + + matchOnDescription?: boolean; + + matchOnDetail?: boolean; +} + +export interface TransferInputBox extends BaseTransferQuickInput { + + type?: 'inputBox'; + + value?: string; + + placeholder?: string; + + password?: boolean; + + buttons?: TransferQuickInputButton[]; + + prompt?: string; + + validationMessage?: string; +} + export interface MainThreadQuickOpenShape extends IDisposable { - $show(multiStepHandle: number | undefined, options: IPickOptions): TPromise; - $setItems(items: MyQuickPickItems[]): TPromise; - $setError(error: Error): TPromise; - $input(multiStepHandle: number | undefined, options: vscode.InputBoxOptions, validateInput: boolean): TPromise; - $multiStep(handle: number): TPromise; + $show(options: IPickOptions, token: CancellationToken): Thenable; + $setItems(items: TransferQuickPickItems[]): Thenable; + $setError(error: Error): Thenable; + $input(options: vscode.InputBoxOptions, validateInput: boolean, token: CancellationToken): Thenable; + $createOrUpdate(params: TransferQuickInput): Thenable; + $dispose(id: number): Thenable; } export interface MainThreadStatusBarShape extends IDisposable { @@ -354,8 +418,8 @@ export interface MainThreadStatusBarShape extends IDisposable { } export interface MainThreadStorageShape extends IDisposable { - $getValue(shared: boolean, key: string): TPromise; - $setValue(shared: boolean, key: string, value: any): TPromise; + $getValue(shared: boolean, key: string): Thenable; + $setValue(shared: boolean, key: string, value: any): Thenable; } export interface MainThreadTelemetryShape extends IDisposable { @@ -364,37 +428,51 @@ export interface MainThreadTelemetryShape extends IDisposable { export type WebviewPanelHandle = string; +export interface WebviewPanelShowOptions { + readonly viewColumn?: EditorViewColumn; + readonly preserveFocus?: boolean; +} + export interface MainThreadWebviewsShape extends IDisposable { - $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, viewOptions: { viewColumn: EditorViewColumn, preserveFocus: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionLocation: UriComponents): void; + $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionLocation: UriComponents): void; $disposeWebview(handle: WebviewPanelHandle): void; - $reveal(handle: WebviewPanelHandle, viewColumn: EditorViewColumn | null, preserveFocus: boolean): void; + $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; $setTitle(handle: WebviewPanelHandle, value: string): void; + $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void; $setHtml(handle: WebviewPanelHandle, value: string): void; + $setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void; $postMessage(handle: WebviewPanelHandle, value: any): Thenable; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; } +export interface WebviewPanelViewState { + readonly active: boolean; + readonly visible: boolean; + readonly position: EditorViewColumn; +} + export interface ExtHostWebviewsShape { $onMessage(handle: WebviewPanelHandle, message: any): void; - $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, active: boolean, position: EditorViewColumn): void; + $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, newState: WebviewPanelViewState): void; $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Thenable; $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: vscode.WebviewOptions): Thenable; } export interface MainThreadUrlsShape extends IDisposable { - $registerProtocolHandler(handle: number, extensionId: string): TPromise; - $unregisterProtocolHandler(handle: number): TPromise; + $registerUriHandler(handle: number, extensionId: string): Thenable; + $unregisterUriHandler(handle: number): Thenable; } export interface ExtHostUrlsShape { - $handleExternalUri(handle: number, uri: UriComponents): TPromise; + $handleExternalUri(handle: number, uri: UriComponents): Thenable; } export interface MainThreadWorkspaceShape extends IDisposable { - $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable; - $cancelSearch(requestId: number): Thenable; + $startFileSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, token: CancellationToken): Thenable; + $startTextSearch(query: IPatternInfo, options: IQueryOptions, requestId: number, token: CancellationToken): Thenable; + $checkExists(includes: string[], token: CancellationToken): Thenable; $saveAll(includeUntitled?: boolean): Thenable; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable; } @@ -407,22 +485,26 @@ export interface IFileChangeDto { export interface MainThreadFileSystemShape extends IDisposable { $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void; $unregisterProvider(handle: number): void; + $setUriFormatter(scheme: string, formatter: LabelRules): void; $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; } export interface MainThreadSearchShape extends IDisposable { - $registerSearchProvider(handle: number, scheme: string): void; + $registerFileSearchProvider(handle: number, scheme: string): void; + $registerTextSearchProvider(handle: number, scheme: string): void; + $registerFileIndexProvider(handle: number, scheme: string): void; $unregisterProvider(handle: number): void; - $handleFindMatch(handle: number, session: number, data: UriComponents | IRawFileMatch2[]): void; + $handleFileMatch(handle: number, session: number, data: UriComponents[]): void; + $handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void; $handleTelemetry(eventName: string, data: any): void; } export interface MainThreadTaskShape extends IDisposable { - $registerTaskProvider(handle: number): TPromise; - $unregisterTaskProvider(handle: number): TPromise; - $fetchTasks(filter?: TaskFilterDTO): TPromise; - $executeTask(task: TaskHandleDTO | TaskDTO): TPromise; - $terminateTask(id: string): TPromise; + $registerTaskProvider(handle: number): Thenable; + $unregisterTaskProvider(handle: number): Thenable; + $fetchTasks(filter?: TaskFilterDTO): Thenable; + $executeTask(task: TaskHandleDTO | TaskDTO): Thenable; + $terminateTask(id: string): Thenable; $registerTaskSystem(scheme: string, info: TaskSystemInfoDTO): void; } @@ -494,18 +576,18 @@ export interface MainThreadDebugServiceShape extends IDisposable { $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $acceptDAError(handle: number, name: string, message: string, stack: string): void; $acceptDAExit(handle: number, code: number, signal: string): void; - $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): TPromise; - $unregisterDebugConfigurationProvider(handle: number): TPromise; - $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise; - $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): TPromise; - $appendDebugConsole(value: string): TPromise; - $startBreakpointEvents(): TPromise; - $registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise; - $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): TPromise; + $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): Thenable; + $unregisterDebugConfigurationProvider(handle: number): Thenable; + $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): Thenable; + $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Thenable; + $appendDebugConsole(value: string): Thenable; + $startBreakpointEvents(): Thenable; + $registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): Thenable; + $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): Thenable; } export interface MainThreadWindowShape extends IDisposable { - $getWindowVisibility(): TPromise; + $getWindowVisibility(): Thenable; } // -- extension host @@ -524,7 +606,7 @@ export interface ExtHostDiagnosticsShape { } export interface ExtHostDocumentContentProvidersShape { - $provideTextDocumentContent(handle: number, uri: UriComponents): TPromise; + $provideTextDocumentContent(handle: number, uri: UriComponents): Promise; } export interface IModelAddedData { @@ -540,7 +622,6 @@ export interface ExtHostDocumentsShape { $acceptModelSaved(strURL: UriComponents): void; $acceptDirtyStateChanged(strURL: UriComponents, isDirty: boolean): void; $acceptModelChanged(strURL: UriComponents, e: IModelChangedEvent, isDirty: boolean): void; - $onDidRename(oldURL: UriComponents, newURL: UriComponents): void; } export interface ExtHostDocumentSaveParticipantShape { @@ -586,35 +667,42 @@ export interface ExtHostDocumentsAndEditorsShape { } export interface ExtHostTreeViewsShape { - $getChildren(treeViewId: string, treeItemHandle?: string): TPromise; + $getChildren(treeViewId: string, treeItemHandle?: string): Thenable; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; + $setVisible(treeViewId: string, visible: boolean): void; } export interface ExtHostWorkspaceShape { $acceptWorkspaceData(workspace: IWorkspaceData): void; + $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void; } export interface ExtHostFileSystemShape { - $stat(handle: number, resource: UriComponents): TPromise; - $readdir(handle: number, resource: UriComponents): TPromise<[string, FileType][]>; - $readFile(handle: number, resource: UriComponents): TPromise; - $writeFile(handle: number, resource: UriComponents, base64Encoded: string, opts: FileWriteOptions): TPromise; - $rename(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): TPromise; - $copy(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): TPromise; - $mkdir(handle: number, resource: UriComponents): TPromise; - $delete(handle: number, resource: UriComponents): TPromise; + $stat(handle: number, resource: UriComponents): Thenable; + $readdir(handle: number, resource: UriComponents): Thenable<[string, FileType][]>; + $readFile(handle: number, resource: UriComponents): Thenable; + $writeFile(handle: number, resource: UriComponents, content: Buffer, opts: FileWriteOptions): Thenable; + $rename(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Thenable; + $copy(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Thenable; + $mkdir(handle: number, resource: UriComponents): Thenable; + $delete(handle: number, resource: UriComponents, opts: FileDeleteOptions): Thenable; $watch(handle: number, session: number, resource: UriComponents, opts: IWatchOptions): void; $unwatch(handle: number, session: number): void; + $open(handle: number, resource: UriComponents): Thenable; + $close(handle: number, fd: number): Thenable; + $read(handle: number, fd: number, pos: number, data: Buffer, offset: number, length: number): Thenable; + $write(handle: number, fd: number, pos: number, data: Buffer, offset: number, length: number): Thenable; } export interface ExtHostSearchShape { - $provideFileSearchResults(handle: number, session: number, query: IRawSearchQuery): TPromise; - $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, query: IRawSearchQuery): TPromise; + $provideFileSearchResults(handle: number, session: number, query: IRawSearchQuery, token: CancellationToken): Thenable; + $clearCache(cacheKey: string): Thenable; + $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, query: IRawSearchQuery, token: CancellationToken): Thenable; } export interface ExtHostExtensionServiceShape { - $activateByEvent(activationEvent: string): TPromise; + $activateByEvent(activationEvent: string): Thenable; } export interface FileSystemEvents { @@ -624,6 +712,8 @@ export interface FileSystemEvents { } export interface ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents): void; + $onFileRename(oldUri: UriComponents, newUri: UriComponents): void; + $onWillRename(oldUri: UriComponents, newUri: UriComponents): Thenable; } export interface ObjectIdentifier { @@ -673,22 +763,28 @@ export interface LocationDto { range: IRange; } -export interface SymbolInformationDto extends IdObject { +export interface DefinitionLinkDto { + origin?: IRange; + uri: UriComponents; + range: IRange; + selectionRange?: IRange; +} + +export interface WorkspaceSymbolDto extends IdObject { name: string; containerName?: string; kind: modes.SymbolKind; location: LocationDto; - definingRange: IRange; - children?: SymbolInformationDto[]; } export interface WorkspaceSymbolsDto extends IdObject { - symbols: SymbolInformationDto[]; + symbols: WorkspaceSymbolDto[]; } export interface ResourceFileEditDto { oldUri: UriComponents; newUri: UriComponents; + options: IFileOperationOptions; } export interface ResourceTextEditDto { @@ -727,38 +823,44 @@ export interface CodeActionDto { } export interface ExtHostLanguageFeaturesShape { - $provideDocumentSymbols(handle: number, resource: UriComponents): TPromise; - $provideCodeLenses(handle: number, resource: UriComponents): TPromise; - $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol): TPromise; - $provideDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideHover(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext): TPromise; - $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext): TPromise; - $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions): TPromise; - $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions): TPromise; - $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise; - $provideWorkspaceSymbols(handle: number, search: string): TPromise; - $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformationDto): TPromise; + $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Thenable; + $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Thenable; + $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol, token: CancellationToken): Thenable; + $provideDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Thenable; + $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Thenable; + $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Thenable; + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Thenable; + $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Thenable; + $provideWorkspaceSymbols(handle: number, search: string, token: CancellationToken): Thenable; + $resolveWorkspaceSymbol(handle: number, symbol: WorkspaceSymbolDto, token: CancellationToken): Thenable; $releaseWorkspaceSymbols(handle: number, id: number): void; - $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string): TPromise; - $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext): TPromise; - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion): TPromise; + $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Thenable; + $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext, token: CancellationToken): Thenable; + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable; $releaseCompletionItems(handle: number, id: number): void; - $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition): TPromise; - $provideDocumentLinks(handle: number, resource: UriComponents): TPromise; - $resolveDocumentLink(handle: number, link: modes.ILink): TPromise; - $provideDocumentColors(handle: number, resource: UriComponents): TPromise; - $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo): TPromise; - $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext): TPromise; + $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable; + $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Thenable; + $resolveDocumentLink(handle: number, link: modes.ILink, token: CancellationToken): Thenable; + $provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): Thenable; + $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo, token: CancellationToken): Thenable; + $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext, token: CancellationToken): Thenable; } export interface ExtHostQuickOpenShape { $onItemSelected(handle: number): void; - $validateInput(input: string): TPromise; + $validateInput(input: string): Thenable; + $onDidChangeActive(sessionId: number, handles: number[]): void; + $onDidChangeSelection(sessionId: number, handles: number[]): void; + $onDidAccept(sessionId: number): void; + $onDidChangeValue(sessionId: number, value: string): void; + $onDidTriggerButton(sessionId: number, handle: number): void; + $onDidHide(sessionId: number): void; } export interface ShellLaunchConfigDto { @@ -772,28 +874,32 @@ export interface ShellLaunchConfigDto { export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number): void; $acceptTerminalOpened(id: number, name: string): void; + $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; $acceptTerminalProcessData(id: number, data: string): void; + $acceptTerminalRendererInput(id: number, data: string): void; + $acceptTerminalRendererDimensions(id: number, cols: number, rows: number): void; $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; - $acceptProcessShutdown(id: number): void; + $acceptProcessShutdown(id: number, immediate: boolean): void; } export interface ExtHostSCMShape { - $provideOriginalResource(sourceControlHandle: number, uri: UriComponents): TPromise; - $onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise; - $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise; - $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined>; + $provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Thenable; + $onInputBoxValueChange(sourceControlHandle: number, value: string): void; + $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): Thenable; + $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Thenable<[string, number] | undefined>; + $setSelectedSourceControls(selectedSourceControlHandles: number[]): Thenable; } export interface ExtHostTaskShape { - $provideTasks(handle: number): TPromise; + $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable; $onDidStartTask(execution: TaskExecutionDTO): void; $onDidStartTaskProcess(value: TaskProcessStartedDTO): void; $onDidEndTaskProcess(value: TaskProcessEndedDTO): void; $OnDidEndTask(execution: TaskExecutionDTO): void; - $resolveVariables(workspaceFolder: URI, variables: string[]): TPromise; + $resolveVariables(workspaceFolder: UriComponents, variables: string[]): Thenable; } export interface IBreakpointDto { @@ -838,16 +944,14 @@ export interface ISourceMultiBreakpointDto { } export interface ExtHostDebugServiceShape { - $substituteVariables(folder: UriComponents | undefined, config: IConfig): TPromise; - $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; - $isTerminalBusy(processId: number): TPromise; - $prepareCommandForTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; - $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null, debugPort: number): TPromise; - $stopDASession(handle: number): TPromise; - $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise; - $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): TPromise; - $provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise; - $debugAdapterExecutable(handle: number, folder: UriComponents | undefined): TPromise; + $substituteVariables(folder: UriComponents | undefined, config: IConfig): Thenable; + $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): Thenable; + $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null, debugPort: number): Thenable; + $stopDASession(handle: number): Thenable; + $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; + $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): Thenable; + $provideDebugConfigurations(handle: number, folder: UriComponents | undefined): Thenable; + $debugAdapterExecutable(handle: number, folder: UriComponents | undefined): Thenable; $acceptDebugSessionStarted(id: DebugSessionUUID, type: string, name: string): void; $acceptDebugSessionTerminated(id: DebugSessionUUID, type: string, name: string): void; $acceptDebugSessionActiveChanged(id: DebugSessionUUID | undefined, type?: string, name?: string): void; @@ -866,7 +970,7 @@ export type DecorationData = [number, boolean, string, string, ThemeColor, strin export type DecorationReply = { [id: number]: DecorationData }; export interface ExtHostDecorationsShape { - $provideDecorations(requests: DecorationRequest[]): TPromise; + $provideDecorations(requests: DecorationRequest[], token: CancellationToken): Thenable; } export interface ExtHostWindowShape { @@ -882,10 +986,10 @@ export interface ExtHostProgressShape { } export interface ExtHostCommentsShape { - $provideDocumentComments(handle: number, document: UriComponents): TPromise; - $createNewCommentThread?(handle: number, document: UriComponents, range: IRange, text: string): TPromise; - $replyToCommentThread?(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): TPromise; - $provideWorkspaceComments(handle: number): TPromise; + $provideDocumentComments(handle: number, document: UriComponents): Thenable; + $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Thenable; + $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Thenable; + $provideWorkspaceComments(handle: number): Thenable; } // --- proxy identifiers diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 0227c874623..caafafe8a78 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import * as types from 'vs/workbench/api/node/extHostTypes'; -import { IRawColorInfo } from 'vs/workbench/api/node/extHost.protocol'; - +import { IRawColorInfo, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; +import * as search from 'vs/workbench/parts/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { IWorkspaceSymbolProvider } from 'vs/workbench/parts/search/common/search'; import { CustomCodeAction } from 'vs/workbench/api/node/extHostLanguageFeatures'; -import { ICommandsExecutor, PreviewHTMLAPICommand, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand } from './apiCommands'; +import { ICommandsExecutor, PreviewHTMLAPICommand, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand, SetEditorLayoutAPICommand } from './apiCommands'; +import { EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; export class ExtHostApiCommands { @@ -111,7 +112,7 @@ export class ExtHostApiCommands { args: [ { name: 'uri', description: 'Uri of a text document', constraint: URI } ], - returns: 'A promise that resolves to an array of SymbolInformation-instances.' + returns: 'A promise that resolves to an array of SymbolInformation and DocumentSymbol instances.' }); this._register('vscode.executeCompletionItemProvider', this._executeCompletionItemProvider, { description: 'Execute completion item provider.', @@ -250,6 +251,13 @@ export class ExtHostApiCommands { { name: 'path', description: 'Path to remove from recently opened.', constraint: (value: any) => typeof value === 'string' } ] }); + + this._register(SetEditorLayoutAPICommand.ID, adjustHandler(SetEditorLayoutAPICommand.execute), { + description: 'Sets the editor layout. The layout is described as object with an initial (optional) orientation (0 = horizontal, 1 = vertical) and an array of editor groups within. Each editor group can have a size and another array of editor groups that will be laid out orthogonal to the orientation. If editor group sizes are provided, their sum must be 1 to be applied per row or column. Example for a 2x2 grid: `{ orientation: 0, groups: [{ groups: [{}, {}], size: 0.5 }, { groups: [{}, {}], size: 0.5 }] }`', + args: [ + { name: 'layout', description: 'The editor layout to set.', constraint: (value: EditorGroupLayout) => typeof value === 'object' && Array.isArray(value.groups) } + ] + }); } // --- command impl @@ -266,11 +274,11 @@ export class ExtHostApiCommands { * @return A promise that resolves to an array of symbol information. */ private _executeWorkspaceSymbolProvider(query: string): Thenable { - return this._commands.executeCommand<[IWorkspaceSymbolProvider, modes.SymbolInformation[]][]>('_executeWorkspaceSymbolProvider', { query }).then(value => { + return this._commands.executeCommand<[search.IWorkspaceSymbolProvider, search.IWorkspaceSymbol[]][]>('_executeWorkspaceSymbolProvider', { query }).then(value => { const result: types.SymbolInformation[] = []; if (Array.isArray(value)) { for (let tuple of value) { - result.push(...tuple[1].map(typeConverters.SymbolInformation.to)); + result.push(...tuple[1].map(typeConverters.WorkspaceSymbol.to)); } } return result; @@ -337,7 +345,7 @@ export class ExtHostApiCommands { position: position && typeConverters.Position.from(position), newName }; - return this._commands.executeCommand('_executeDocumentRenameProvider', args).then(value => { + return this._commands.executeCommand('_executeDocumentRenameProvider', args).then(value => { if (!value) { return undefined; } @@ -404,15 +412,35 @@ export class ExtHostApiCommands { }); } - private _executeDocumentSymbolProvider(resource: URI): Thenable { + private _executeDocumentSymbolProvider(resource: URI): Thenable { const args = { resource }; - return this._commands.executeCommand('_executeDocumentSymbolProvider', args).then(value => { - if (value && Array.isArray(value.entries)) { - return value.entries.map(typeConverters.SymbolInformation.to); + return this._commands.executeCommand('_executeDocumentSymbolProvider', args).then(value => { + if (isFalsyOrEmpty(value)) { + return undefined; } - return undefined; + class MergedInfo extends types.SymbolInformation implements vscode.DocumentSymbol { + static to(symbol: modes.DocumentSymbol): MergedInfo { + let res = new MergedInfo( + symbol.name, + typeConverters.SymbolKind.to(symbol.kind), + symbol.containerName, + new types.Location(resource, typeConverters.Range.to(symbol.range)) + ); + res.detail = symbol.detail; + res.range = res.location.range; + res.selectionRange = typeConverters.Range.to(symbol.selectionRange); + res.children = symbol.children && symbol.children.map(MergedInfo.to); + return res; + } + + detail: string; + range: vscode.Range; + selectionRange: vscode.Range; + children: vscode.DocumentSymbol[]; + } + return value.map(MergedInfo.to); }); } diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/node/extHostCommands.ts index e1607cd562e..9eb67f348a4 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/node/extHostCommands.ts @@ -131,6 +131,8 @@ export class ExtHostCommands implements ExtHostCommandsShape { } $executeContributedCommand(id: string, ...args: any[]): Thenable { + this._logService.trace('ExtHostCommands#$executeContributedCommand', id); + if (!this._commands.has(id)) { return Promise.reject(new Error(`Contributed command '${id}' does not exist.`)); } else { diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index a6a85ba54e0..c981115c0a6 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -5,8 +5,8 @@ 'use strict'; -import { asWinJsPromise } from 'vs/base/common/async'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { asThenable } from 'vs/base/common/async'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import * as modes from 'vs/editor/common/modes'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; @@ -15,6 +15,7 @@ import * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol'; import { CommandsConverter } from './extHostCommands'; import { IRange } from 'vs/editor/common/core/range'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostComments implements ExtHostCommentsShape { private static handlePool = 0; @@ -64,7 +65,7 @@ export class ExtHostComments implements ExtHostCommentsShape { }; } - $createNewCommentThread(handle: number, uri: UriComponents, range: IRange, text: string): TPromise { + $createNewCommentThread(handle: number, uri: UriComponents, range: IRange, text: string): Thenable { const data = this._documents.getDocumentData(URI.revive(uri)); const ran = extHostTypeConverter.Range.to(range); @@ -72,13 +73,13 @@ export class ExtHostComments implements ExtHostCommentsShape { return TPromise.as(null); } - return asWinJsPromise(token => { + return asThenable(() => { let provider = this._documentProviders.get(handle); - return provider.createNewCommentThread(data.document, ran, text, token); + return provider.createNewCommentThread(data.document, ran, text, CancellationToken.None); }).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); } - $replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): TPromise { + $replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): Thenable { const data = this._documents.getDocumentData(URI.revive(uri)); const ran = extHostTypeConverter.Range.to(range); @@ -86,33 +87,33 @@ export class ExtHostComments implements ExtHostCommentsShape { return TPromise.as(null); } - return asWinJsPromise(token => { + return asThenable(() => { let provider = this._documentProviders.get(handle); - return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, token); + return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, CancellationToken.None); }).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); } - $provideDocumentComments(handle: number, uri: UriComponents): TPromise { + $provideDocumentComments(handle: number, uri: UriComponents): Thenable { const data = this._documents.getDocumentData(URI.revive(uri)); if (!data || !data.document) { return TPromise.as(null); } - return asWinJsPromise(token => { + return asThenable(() => { let provider = this._documentProviders.get(handle); - return provider.provideDocumentComments(data.document, token); + return provider.provideDocumentComments(data.document, CancellationToken.None); }) .then(commentInfo => commentInfo ? convertCommentInfo(handle, commentInfo, this._commandsConverter) : null); } - $provideWorkspaceComments(handle: number): TPromise { + $provideWorkspaceComments(handle: number): Thenable { const provider = this._workspaceProviders.get(handle); if (!provider) { return TPromise.as(null); } - return asWinJsPromise(token => { - return provider.provideWorkspaceComments(token); + return asThenable(() => { + return provider.provideWorkspaceComments(CancellationToken.None); }).then(comments => comments.map(x => convertToCommentThread(x, this._commandsConverter) )); diff --git a/src/vs/workbench/api/node/extHostConfiguration.ts b/src/vs/workbench/api/node/extHostConfiguration.ts index 6798aad682a..c41f834005e 100644 --- a/src/vs/workbench/api/node/extHostConfiguration.ts +++ b/src/vs/workbench/api/node/extHostConfiguration.ts @@ -5,7 +5,7 @@ 'use strict'; import { mixin, deepClone } from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import * as vscode from 'vscode'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 4252cfab0b3..6bd5c081c1c 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -6,10 +6,11 @@ import * as paths from 'vs/base/common/paths'; import { Schemas } from 'vs/base/common/network'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { asThenable } from 'vs/base/common/async'; +import * as nls from 'vs/nls'; import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto @@ -24,11 +25,13 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumen import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution, IConfig, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; import { getTerminalLauncher, hasChildprocesses, prepareCommand } from 'vs/workbench/parts/debug/node/terminals'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { ExtHostConfiguration } from './extHostConfiguration'; 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'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostDebugService implements ExtHostDebugServiceShape { @@ -66,12 +69,16 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private _variableResolver: IConfigurationResolverService; + private _integratedTerminalInstance: vscode.Terminal; + private _terminalDisposedListener: IDisposable; + constructor(mainContext: IMainContext, private _workspaceService: ExtHostWorkspace, private _extensionService: ExtHostExtensionService, private _editorsService: ExtHostDocumentsAndEditors, - private _configurationService: ExtHostConfiguration + private _configurationService: ExtHostConfiguration, + private _terminalService: ExtHostTerminalService ) { this._handleCounter = 0; @@ -118,35 +125,72 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } public $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - const terminalLauncher = getTerminalLauncher(); - if (terminalLauncher) { - return terminalLauncher.runInTerminal(args, config); + + if (args.kind === 'integrated') { + + if (!this._terminalDisposedListener) { + // React on terminal disposed and check if that is the debug terminal #12956 + this._terminalDisposedListener = this._terminalService.onDidCloseTerminal(terminal => { + if (this._integratedTerminalInstance && this._integratedTerminalInstance === terminal) { + this._integratedTerminalInstance = null; + } + }); + } + + return new TPromise(resolve => { + if (this._integratedTerminalInstance) { + this._integratedTerminalInstance.processId.then(pid => { + resolve(hasChildprocesses(pid)); + }, err => { + resolve(true); + }); + } else { + resolve(true); + } + }).then(needNewTerminal => { + + if (needNewTerminal) { + this._integratedTerminalInstance = this._terminalService.createTerminal(args.title || nls.localize('debug.terminal.title', "debuggee")); + } + + this._integratedTerminalInstance.show(); + + return new TPromise((resolve, error) => { + setTimeout(_ => { + const command = prepareCommand(args, config); + this._integratedTerminalInstance.sendText(command, true); + resolve(void 0); + }, 500); + }); + }); + + } else if (args.kind === 'external') { + + const terminalLauncher = getTerminalLauncher(); + if (terminalLauncher) { + return terminalLauncher.runInTerminal(args, config); + } } return void 0; } - public $isTerminalBusy(processId: number): TPromise { - return asWinJsPromise(token => hasChildprocesses(processId)); - } - - public $prepareCommandForTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return asWinJsPromise(token => prepareCommand(args, config)); - } - public $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): TPromise { if (!this._variableResolver) { this._variableResolver = new ExtHostVariableResolverService(this._workspaceService, this._editorsService, this._configurationService); } + let ws: IWorkspaceFolder; const folder = this.getFolder(folderUri); - let ws: IWorkspaceFolder = { - uri: folder.uri, - name: folder.name, - index: folder.index, - toResource: () => { - throw new Error('Not implemented'); - } - }; - return asWinJsPromise(token => DebugAdapter.substituteVariables(ws, config, this._variableResolver)); + if (folder) { + ws = { + uri: folder.uri, + name: folder.name, + index: folder.index, + toResource: () => { + throw new Error('Not implemented'); + } + }; + } + return TPromise.wrap(this._variableResolver.resolveAny(ws, config)); } public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null, debugPort: number): TPromise { @@ -292,7 +336,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this.fireBreakpointChanges(a, r, c); } - public addBreakpoints(breakpoints0: vscode.Breakpoint[]): TPromise { + public addBreakpoints(breakpoints0: vscode.Breakpoint[]): Thenable { this.startBreakpoints(); @@ -358,7 +402,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { return this._debugServiceProxy.$registerBreakpoints(dtos); } - public removeBreakpoints(breakpoints0: vscode.Breakpoint[]): TPromise { + public removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Thenable { this.startBreakpoints(); @@ -383,9 +427,9 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private fireBreakpointChanges(added: vscode.Breakpoint[], removed: vscode.Breakpoint[], changed: vscode.Breakpoint[]) { if (added.length > 0 || removed.length > 0 || changed.length > 0) { this._onDidChangeBreakpoints.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed), - changed: Object.freeze(changed) + added, + removed, + changed, })); } } @@ -408,7 +452,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { }); } - public $provideDebugConfigurations(handle: number, folderUri: UriComponents | undefined): TPromise { + public $provideDebugConfigurations(handle: number, folderUri: UriComponents | undefined): Thenable { let handler = this._handlers.get(handle); if (!handler) { return TPromise.wrapError(new Error('no handler found')); @@ -416,10 +460,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { if (!handler.provideDebugConfigurations) { return TPromise.wrapError(new Error('handler has no method provideDebugConfigurations')); } - return asWinJsPromise(token => handler.provideDebugConfigurations(this.getFolder(folderUri), token)); + return asThenable(() => handler.provideDebugConfigurations(this.getFolder(folderUri), CancellationToken.None)); } - public $resolveDebugConfiguration(handle: number, folderUri: UriComponents | undefined, debugConfiguration: vscode.DebugConfiguration): TPromise { + public $resolveDebugConfiguration(handle: number, folderUri: UriComponents | undefined, debugConfiguration: vscode.DebugConfiguration): Thenable { let handler = this._handlers.get(handle); if (!handler) { return TPromise.wrapError(new Error('no handler found')); @@ -427,10 +471,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { if (!handler.resolveDebugConfiguration) { return TPromise.wrapError(new Error('handler has no method resolveDebugConfiguration')); } - return asWinJsPromise(token => handler.resolveDebugConfiguration(this.getFolder(folderUri), debugConfiguration, token)); + return asThenable(() => handler.resolveDebugConfiguration(this.getFolder(folderUri), debugConfiguration, CancellationToken.None)); } - public $debugAdapterExecutable(handle: number, folderUri: UriComponents | undefined): TPromise { + public $debugAdapterExecutable(handle: number, folderUri: UriComponents | undefined): Thenable { let handler = this._handlers.get(handle); if (!handler) { return TPromise.wrapError(new Error('no handler found')); @@ -438,10 +482,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { if (!handler.debugAdapterExecutable) { return TPromise.wrapError(new Error('handler has no method debugAdapterExecutable')); } - return asWinJsPromise(token => handler.debugAdapterExecutable(this.getFolder(folderUri), token)); + return asThenable(() => handler.debugAdapterExecutable(this.getFolder(folderUri), CancellationToken.None)); } - public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise { + public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration): Thenable { return this._debugServiceProxy.$startDebugging(folder ? folder.uri : undefined, nameOrConfig); } @@ -495,7 +539,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._onDidReceiveDebugSessionCustomEvent.fire(ee); } - private getFolder(_folderUri: UriComponents | undefined): vscode.WorkspaceFolder { + private getFolder(_folderUri: UriComponents | undefined): vscode.WorkspaceFolder | undefined { if (_folderUri) { const folderURI = URI.revive(_folderUri); return this._workspaceService.resolveWorkspaceFolder(folderURI); @@ -558,15 +602,12 @@ export class ExtHostDebugConsole implements vscode.DebugConsole { } } -export class ExtHostVariableResolverService implements IConfigurationResolverService { +export class ExtHostVariableResolverService extends AbstractVariableResolverService { - _serviceBrand: any; - _variableResolver: VariableResolver; - - constructor(workspace: ExtHostWorkspace, editors: ExtHostDocumentsAndEditors, configuration: ExtHostConfiguration) { - this._variableResolver = new VariableResolver({ + constructor(workspaceService: ExtHostWorkspace, editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfiguration) { + super({ getFolderUri: (folderName: string): URI => { - const folders = workspace.getWorkspaceFolders(); + const folders = workspaceService.getWorkspaceFolders(); const found = folders.filter(f => f.name === folderName); if (found && found.length > 0) { return found[0].uri; @@ -574,16 +615,16 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer return undefined; }, getWorkspaceFolderCount: (): number => { - return workspace.getWorkspaceFolders().length; + return workspaceService.getWorkspaceFolders().length; }, getConfigurationValue: (folderUri: URI, section: string) => { - return configuration.getConfiguration(undefined, folderUri).get(section); + return configurationService.getConfiguration(undefined, folderUri).get(section); }, getExecPath: (): string | undefined => { return undefined; // does not exist in EH }, getFilePath: (): string | undefined => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor) { const resource = activeEditor.document.uri; if (resource.scheme === Schemas.file) { @@ -593,34 +634,19 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer return undefined; }, getSelectedText: (): string | undefined => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor && !activeEditor.selection.isEmpty) { return activeEditor.document.getText(activeEditor.selection); } return undefined; }, getLineNumber: (): string => { - const activeEditor = editors.activeEditor(); + const activeEditor = editorService.activeEditor(); if (activeEditor) { return String(activeEditor.selection.end.line + 1); } return undefined; } - }, process.env); - } - - public resolve(root: IWorkspaceFolder, value: string): string; - public resolve(root: IWorkspaceFolder, value: string[]): string[]; - public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; - public resolve(root: IWorkspaceFolder, value: any): any { - return this._variableResolver.resolveAny(root ? root.uri : undefined, value); - } - - public resolveAny(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary): T { - return this._variableResolver.resolveAny(root ? root.uri : undefined, value, commandMapping); - } - - public executeCommandVariables(configuration: any, variables: IStringDictionary): TPromise> { - throw new Error('findAndExecuteCommandVariables not implemented.'); + }); } } diff --git a/src/vs/workbench/api/node/extHostDecorations.ts b/src/vs/workbench/api/node/extHostDecorations.ts index 77d9b441fc8..0e402fd31b3 100644 --- a/src/vs/workbench/api/node/extHostDecorations.ts +++ b/src/vs/workbench/api/node/extHostDecorations.ts @@ -5,27 +5,32 @@ 'use strict'; import * as vscode from 'vscode'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { MainContext, IMainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, DecorationData, DecorationRequest, DecorationReply } from 'vs/workbench/api/node/extHost.protocol'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable } from 'vs/workbench/api/node/extHostTypes'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ProviderData { + provider: vscode.DecorationProvider; + extensionId: string; +} export class ExtHostDecorations implements ExtHostDecorationsShape { private static _handlePool = 0; - private readonly _provider = new Map(); + private readonly _provider = new Map(); private readonly _proxy: MainThreadDecorationsShape; constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadDecorations); } - registerDecorationProvider(provider: vscode.DecorationProvider, label: string): vscode.Disposable { + registerDecorationProvider(provider: vscode.DecorationProvider, extensionId: string): vscode.Disposable { const handle = ExtHostDecorations._handlePool++; - this._provider.set(handle, provider); - this._proxy.$registerDecorationProvider(handle, label); + this._provider.set(handle, { provider, extensionId }); + this._proxy.$registerDecorationProvider(handle, extensionId); const listener = provider.onDidChangeDecorations(e => { this._proxy.$onDidChange(handle, !e ? null : Array.isArray(e) ? e : [e]); @@ -38,17 +43,20 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { }); } - $provideDecorations(requests: DecorationRequest[]): TPromise { + $provideDecorations(requests: DecorationRequest[], token: CancellationToken): Thenable { const result: DecorationReply = Object.create(null); return TPromise.join(requests.map(request => { const { handle, uri, id } = request; - const provider = this._provider.get(handle); - if (!provider) { + if (!this._provider.has(handle)) { // might have been unregistered in the meantime return void 0; } - return asWinJsPromise(token => provider.provideDecoration(URI.revive(uri), token)).then(data => { - result[id] = data && [data.priority, data.bubble, data.title, data.abbreviation, data.color, data.source]; + const { provider, extensionId } = this._provider.get(handle); + return Promise.resolve(provider.provideDecoration(URI.revive(uri), token)).then(data => { + if (data && data.letter && data.letter.length !== 1) { + console.warn(`INVALID decoration from extension '${extensionId}'. The 'letter' must be set and be one character, not '${data.letter}'.`); + } + result[id] = data && [data.priority, data.bubble, data.title, data.letter, data.color, data.source]; }, err => { console.error(err); }); diff --git a/src/vs/workbench/api/node/extHostDiagnostics.ts b/src/vs/workbench/api/node/extHostDiagnostics.ts index 9056054629e..0ceac3df023 100644 --- a/src/vs/workbench/api/node/extHostDiagnostics.ts +++ b/src/vs/workbench/api/node/extHostDiagnostics.ts @@ -6,27 +6,29 @@ import { localize } from 'vs/nls'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as vscode from 'vscode'; import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol'; -import { DiagnosticSeverity } from './extHostTypes'; +import { DiagnosticSeverity, Diagnostic } from './extHostTypes'; import * as converter from './extHostTypeConverters'; -import { mergeSort } from 'vs/base/common/arrays'; +import { mergeSort, equals } from 'vs/base/common/arrays'; import { Event, Emitter, debounceEvent, mapEvent } from 'vs/base/common/event'; import { keys } from 'vs/base/common/map'; export class DiagnosticCollection implements vscode.DiagnosticCollection { private readonly _name: string; + private readonly _owner: string; private readonly _maxDiagnosticsPerFile: number; private readonly _onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>; + private readonly _proxy: MainThreadDiagnosticsShape; - private _proxy: MainThreadDiagnosticsShape; private _isDisposed = false; private _data = new Map(); - constructor(name: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { + constructor(name: string, owner: string, maxDiagnosticsPerFile: number, proxy: MainThreadDiagnosticsShape, onDidChangeDiagnostics: Emitter<(vscode.Uri | string)[]>) { this._name = name; + this._owner = owner; this._maxDiagnosticsPerFile = maxDiagnosticsPerFile; this._proxy = proxy; this._onDidChangeDiagnostics = onDidChangeDiagnostics; @@ -35,8 +37,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { dispose(): void { if (!this._isDisposed) { this._onDidChangeDiagnostics.fire(keys(this._data)); - this._proxy.$clear(this.name); - this._proxy = undefined; + this._proxy.$clear(this._owner); this._data = undefined; this._isDisposed = true; } @@ -61,9 +62,13 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { this._checkDisposed(); let toSync: vscode.Uri[]; + let hasChanged = true; if (first instanceof URI) { + // check if this has actually changed + hasChanged = hasChanged && !equals(diagnostics, this.get(first), Diagnostic.isEqual); + if (!diagnostics) { // remove this entry this.delete(first); @@ -102,6 +107,17 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { } } + // send event for extensions + this._onDidChangeDiagnostics.fire(toSync); + + // if nothing has changed then there is nothing else to do + // we have updated the diagnostics but we don't send a message + // to the renderer. tho we have still send an event for other + // extensions because the diagnostic might carry more information + // than known to us + if (!hasChanged) { + return; + } // compute change and send to main side const entries: [URI, IMarkerData[]][] = []; for (let uri of toSync) { @@ -141,22 +157,21 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { entries.push([uri, marker]); } - this._onDidChangeDiagnostics.fire(toSync); - this._proxy.$changeMany(this.name, entries); + this._proxy.$changeMany(this._owner, entries); } delete(uri: vscode.Uri): void { this._checkDisposed(); this._onDidChangeDiagnostics.fire([uri]); this._data.delete(uri.toString()); - this._proxy.$changeMany(this.name, [[uri, undefined]]); + this._proxy.$changeMany(this._owner, [[uri, undefined]]); } clear(): void { this._checkDisposed(); this._onDidChangeDiagnostics.fire(keys(this._data)); this._data.clear(); - this._proxy.$clear(this.name); + this._proxy.$clear(this._owner); } forEach(callback: (uri: URI, diagnostics: vscode.Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void { @@ -204,7 +219,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private static readonly _maxDiagnosticsPerFile: number = 1000; private readonly _proxy: MainThreadDiagnosticsShape; - private readonly _collections: DiagnosticCollection[] = []; + private readonly _collections = new Map(); private readonly _onDidChangeDiagnostics = new Emitter<(vscode.Uri | string)[]>(); static _debouncer(last: (vscode.Uri | string)[], current: (vscode.Uri | string)[]): (vscode.Uri | string)[] { @@ -242,22 +257,28 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } createDiagnosticCollection(name: string): vscode.DiagnosticCollection { + let { _collections, _proxy, _onDidChangeDiagnostics } = this; + let owner: string; if (!name) { name = '_generated_diagnostic_collection_name_#' + ExtHostDiagnostics._idPool++; + owner = name; + } else if (!_collections.has(name)) { + owner = name; + } else { + console.warn(`DiagnosticCollection with name '${name}' does already exist.`); + do { + owner = name + ExtHostDiagnostics._idPool++; + } while (_collections.has(owner)); } - const { _collections, _proxy, _onDidChangeDiagnostics } = this; const result = new class extends DiagnosticCollection { constructor() { - super(name, ExtHostDiagnostics._maxDiagnosticsPerFile, _proxy, _onDidChangeDiagnostics); - _collections.push(this); + super(name, owner, ExtHostDiagnostics._maxDiagnosticsPerFile, _proxy, _onDidChangeDiagnostics); + _collections.set(owner, this); } dispose() { super.dispose(); - let idx = _collections.indexOf(this); - if (idx !== -1) { - _collections.splice(idx, 1); - } + _collections.delete(owner); } }; @@ -272,7 +293,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } else { let index = new Map(); let res: [vscode.Uri, vscode.Diagnostic[]][] = []; - for (const collection of this._collections) { + this._collections.forEach(collection => { collection.forEach((uri, diagnostics) => { let idx = index.get(uri.toString()); if (typeof idx === 'undefined') { @@ -282,18 +303,18 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { } res[idx][1] = res[idx][1].concat(...diagnostics); }); - } + }); return res; } } private _getDiagnostics(resource: vscode.Uri): vscode.Diagnostic[] { let res: vscode.Diagnostic[] = []; - for (const collection of this._collections) { + this._collections.forEach(collection => { if (collection.has(resource)) { res = res.concat(collection.get(resource)); } - } + }); return res; } } diff --git a/src/vs/workbench/api/node/extHostDialogs.ts b/src/vs/workbench/api/node/extHostDialogs.ts index 70360049526..4def91a52e7 100644 --- a/src/vs/workbench/api/node/extHostDialogs.ts +++ b/src/vs/workbench/api/node/extHostDialogs.ts @@ -5,7 +5,7 @@ 'use strict'; import * as vscode from 'vscode'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { MainContext, MainThreadDiaglogsShape, IMainContext } from 'vs/workbench/api/node/extHost.protocol'; export class ExtHostDialogs { diff --git a/src/vs/workbench/api/node/extHostDocumentContentProviders.ts b/src/vs/workbench/api/node/extHostDocumentContentProviders.ts index 8086a431f29..ee8cbe11120 100644 --- a/src/vs/workbench/api/node/extHostDocumentContentProviders.ts +++ b/src/vs/workbench/api/node/extHostDocumentContentProviders.ts @@ -5,16 +5,15 @@ 'use strict'; import { onUnexpectedError } from 'vs/base/common/errors'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/workbench/api/node/extHostTypes'; -import { TPromise } from 'vs/base/common/winjs.base'; import * as vscode from 'vscode'; -import { asWinJsPromise } from 'vs/base/common/async'; import { MainContext, ExtHostDocumentContentProvidersShape, MainThreadDocumentContentProvidersShape, IMainContext } from './extHost.protocol'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostDocumentContentProvider implements ExtHostDocumentContentProvidersShape { @@ -86,11 +85,11 @@ export class ExtHostDocumentContentProvider implements ExtHostDocumentContentPro }); } - $provideTextDocumentContent(handle: number, uri: UriComponents): TPromise { + $provideTextDocumentContent(handle: number, uri: UriComponents): Promise { const provider = this._documentContentProviders.get(handle); if (!provider) { - return TPromise.wrapError(new Error(`unsupported uri-scheme: ${uri.scheme}`)); + return Promise.reject(new Error(`unsupported uri-scheme: ${uri.scheme}`)); } - return asWinJsPromise(token => provider.provideTextDocumentContent(URI.revive(uri), token)); + return Promise.resolve(provider.provideTextDocumentContent(URI.revive(uri), CancellationToken.None)); } } diff --git a/src/vs/workbench/api/node/extHostDocumentData.ts b/src/vs/workbench/api/node/extHostDocumentData.ts index 2eeef2a7d5f..fec31cda756 100644 --- a/src/vs/workbench/api/node/extHostDocumentData.ts +++ b/src/vs/workbench/api/node/extHostDocumentData.ts @@ -7,7 +7,7 @@ import { ok } from 'vs/base/common/assert'; import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; import * as vscode from 'vscode'; import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; @@ -99,7 +99,7 @@ export class ExtHostDocumentData extends MirrorTextModel { this._isDirty = isDirty; } - private _save(): TPromise { + private _save(): Thenable { if (this._isDisposed) { return TPromise.wrapError(new Error('Document has been closed')); } diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index 4d0a156a617..3b729f70916 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -5,7 +5,7 @@ 'use strict'; import { Event } from 'vs/base/common/event'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { sequence, always } from 'vs/base/common/async'; import { illegalState } from 'vs/base/common/errors'; import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, ResourceTextEditDto } from 'vs/workbench/api/node/extHost.protocol'; diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/node/extHostDocuments.ts index e9122c83d72..53303a27602 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/node/extHostDocuments.ts @@ -5,7 +5,7 @@ 'use strict'; import { Event, Emitter } from 'vs/base/common/event'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as TypeConverters from './extHostTypeConverters'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -21,18 +21,16 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { private _onDidRemoveDocument = new Emitter(); private _onDidChangeDocument = new Emitter(); private _onDidSaveDocument = new Emitter(); - private _onDidRenameResource = new Emitter(); readonly onDidAddDocument: Event = this._onDidAddDocument.event; readonly onDidRemoveDocument: Event = this._onDidRemoveDocument.event; readonly onDidChangeDocument: Event = this._onDidChangeDocument.event; readonly onDidSaveDocument: Event = this._onDidSaveDocument.event; - readonly onDidRenameResource: Event = this._onDidRenameResource.event; private _toDispose: IDisposable[]; private _proxy: MainThreadDocumentsShape; private _documentsAndEditors: ExtHostDocumentsAndEditors; - private _documentLoader = new Map>(); + private _documentLoader = new Map>(); constructor(mainContext: IMainContext, documentsAndEditors: ExtHostDocumentsAndEditors) { this._proxy = mainContext.getProxy(MainContext.MainThreadDocuments); @@ -71,7 +69,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { return undefined; } - public ensureDocumentData(uri: URI): TPromise { + public ensureDocumentData(uri: URI): Thenable { let cached = this._documentsAndEditors.getDocument(uri.toString()); if (cached) { @@ -93,7 +91,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { return promise; } - public createDocumentData(options?: { language?: string; content?: string }): TPromise { + public createDocumentData(options?: { language?: string; content?: string }): Thenable { return this._proxy.$tryCreateDocument(options).then(data => URI.revive(data)); } @@ -150,11 +148,4 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { public setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { setWordDefinitionFor(modeId, wordDefinition); } - - public $onDidRename(oldURL: UriComponents, newURL: UriComponents): void { - const oldResource = URI.revive(oldURL); - const newResource = URI.revive(newURL); - this._onDidRenameResource.fire({ oldResource, newResource }); - } - } diff --git a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts index ac627c91ece..87e6a76a3a0 100644 --- a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts @@ -11,7 +11,7 @@ import { ExtHostDocumentData } from './extHostDocumentData'; import { ExtHostTextEditor } from './extHostTextEditor'; import * as assert from 'assert'; import * as typeConverters from './extHostTypeConverters'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Disposable } from './extHostTypes'; export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape { diff --git a/src/vs/workbench/api/node/extHostExtensionActivator.ts b/src/vs/workbench/api/node/extHostExtensionActivator.ts index c37e7b2dfe7..5218f66cda6 100644 --- a/src/vs/workbench/api/node/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/node/extHostExtensionActivator.ts @@ -10,7 +10,6 @@ import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtHostLogger } from 'vs/workbench/api/node/extHostLogService'; const hasOwnProperty = Object.hasOwnProperty; const NO_OP_VOID_PROMISE = TPromise.wrap(void 0); @@ -27,8 +26,7 @@ export interface IExtensionContext { extensionPath: string; storagePath: string; asAbsolutePath(relativePath: string): string; - logger: ExtHostLogger; - readonly logDirectory: string; + readonly logPath: string; } /** diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index a274837583e..ecfdec1b3d1 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -12,8 +12,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; -import { createApiFactory, initializeExtensionApi, checkProposedApiEnabled } from 'vs/workbench/api/node/extHost.api.impl'; -import { MainContext, MainThreadExtensionServiceShape, IWorkspaceData, IEnvironment, IInitData, ExtHostExtensionServiceShape, MainThreadTelemetryShape, IExtHostContext } from './extHost.protocol'; +import { createApiFactory, initializeExtensionApi } from 'vs/workbench/api/node/extHost.api.impl'; +import { MainContext, MainThreadExtensionServiceShape, IWorkspaceData, IEnvironment, IInitData, ExtHostExtensionServiceShape, MainThreadTelemetryShape, IMainContext } from './extHost.protocol'; import { IExtensionMemento, ExtensionsActivator, ActivatedExtension, IExtensionAPI, IExtensionContext, EmptyExtension, IExtensionModule, ExtensionActivationTimesBuilder, ExtensionActivationTimes, ExtensionActivationReason, ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; @@ -21,7 +21,7 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; class ExtensionMemento implements IExtensionMemento { @@ -29,7 +29,7 @@ class ExtensionMemento implements IExtensionMemento { private readonly _shared: boolean; private readonly _storage: ExtHostStorage; - private readonly _init: TPromise; + private readonly _init: Thenable; private _value: { [n: string]: any; }; constructor(id: string, global: boolean, storage: ExtHostStorage) { @@ -43,7 +43,7 @@ class ExtensionMemento implements IExtensionMemento { }); } - get whenReady(): TPromise { + get whenReady(): Thenable { return this._init; } @@ -68,7 +68,7 @@ class ExtensionStoragePath { private readonly _workspace: IWorkspaceData; private readonly _environment: IEnvironment; - private readonly _ready: TPromise; + private readonly _ready: Promise; private _value: string; constructor(workspace: IWorkspaceData, environment: IEnvironment) { @@ -77,7 +77,7 @@ class ExtensionStoragePath { this._ready = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value); } - get whenReady(): TPromise { + get whenReady(): Promise { return this._ready; } @@ -88,13 +88,13 @@ class ExtensionStoragePath { return undefined; } - private async _getOrCreateWorkspaceStoragePath(): TPromise { + private async _getOrCreateWorkspaceStoragePath(): Promise { if (!this._workspace) { return TPromise.as(undefined); } const storageName = this._workspace.id; - const storagePath = join(this._environment.appSettingsHome, 'workspaceStorage', storageName); + const storagePath = join(this._environment.appSettingsHome.fsPath, 'workspaceStorage', storageName); const exists = await dirExists(storagePath); @@ -136,7 +136,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { * This class is constructed manually because it is a service, so it doesn't use any ctor injection */ constructor(initData: IInitData, - extHostContext: IExtHostContext, + extHostContext: IMainContext, extHostWorkspace: ExtHostWorkspace, extHostConfiguration: ExtHostConfiguration, extHostLogService: ExtHostLogService @@ -361,14 +361,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, storagePath: this._storagePath.value(extensionDescription), asAbsolutePath: (relativePath: string) => { return join(extensionDescription.extensionLocation.fsPath, relativePath); }, - get logger() { - checkProposedApiEnabled(extensionDescription); - return that._extHostLogService.getExtLogger(extensionDescription.id); - }, - get logDirectory() { - checkProposedApiEnabled(extensionDescription); - return that._extHostLogService.getLogDirectory(extensionDescription.id); - } + logPath: that._extHostLogService.getLogDirectory(extensionDescription.id) }); }); } @@ -409,7 +402,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { // -- called by main thread - public $activateByEvent(activationEvent: string): TPromise { + public $activateByEvent(activationEvent: string): Thenable { return this.activateByEvent(activationEvent, false); } } diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index 2fa4cc8844f..00a379eb808 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -4,17 +4,16 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol'; import * as vscode from 'vscode'; import * as files from 'vs/platform/files/common/files'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import { Range, FileChangeType } from 'vs/workbench/api/node/extHostTypes'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; import { Schemas } from 'vs/base/common/network'; +import { LabelRules } from 'vs/platform/label/common/label'; class FsLinkProvider implements vscode.DocumentLinkProvider { @@ -83,7 +82,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { extHostLanguageFeatures.registerDocumentLinkProvider('*', this._linkProvider); } - registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean } = {}) { + registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { if (this._usedSchemes.has(scheme)) { throw new Error(`a provider for the scheme '${scheme}' is already registered`); @@ -98,9 +97,17 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { if (options.isCaseSensitive) { capabilites += files.FileSystemProviderCapabilities.PathCaseSensitive; } + if (options.isReadonly) { + capabilites += files.FileSystemProviderCapabilities.Readonly; + } if (typeof provider.copy === 'function') { capabilites += files.FileSystemProviderCapabilities.FileFolderCopy; } + if (typeof provider.open === 'function' && typeof provider.close === 'function' + && typeof provider.read === 'function' && typeof provider.write === 'function' + ) { + capabilites += files.FileSystemProviderCapabilities.FileOpenReadWriteClose; + } this._proxy.$registerFileSystemProvider(handle, scheme, capabilites); @@ -129,15 +136,17 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._proxy.$onFileSystemChange(handle, mapped); }); - return { - dispose: () => { - subscription.dispose(); - this._linkProvider.delete(scheme); - this._usedSchemes.delete(scheme); - this._fsProvider.delete(handle); - this._proxy.$unregisterProvider(handle); - } - }; + return toDisposable(() => { + subscription.dispose(); + this._linkProvider.delete(scheme); + this._usedSchemes.delete(scheme); + this._fsProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + + setUriFormatter(scheme: string, formatter: LabelRules): void { + this._proxy.$setUriFormatter(scheme, formatter); } private static _asIStat(stat: vscode.FileStat): files.IStat { @@ -145,47 +154,43 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { return { type, ctime, mtime, size }; } - $stat(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat); + $stat(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._fsProvider.get(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat); } - $readdir(handle: number, resource: UriComponents): TPromise<[string, files.FileType][], any> { - return asWinJsPromise(() => this._fsProvider.get(handle).readDirectory(URI.revive(resource))); + $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> { + return Promise.resolve(this._fsProvider.get(handle).readDirectory(URI.revive(resource))); } - $readFile(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(() => { - return this._fsProvider.get(handle).readFile(URI.revive(resource)); - }).then(data => { - return Buffer.isBuffer(data) ? data.toString('base64') : Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('base64'); + $readFile(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._fsProvider.get(handle).readFile(URI.revive(resource))).then(data => { + return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); }); } - $writeFile(handle: number, resource: UriComponents, base64Content: string, opts: files.FileWriteOptions): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).writeFile(URI.revive(resource), Buffer.from(base64Content, 'base64'), opts)); + $writeFile(handle: number, resource: UriComponents, content: Buffer, opts: files.FileWriteOptions): Promise { + return Promise.resolve(this._fsProvider.get(handle).writeFile(URI.revive(resource), content, opts)); } - $delete(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).delete(URI.revive(resource), { recursive: true })); + $delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise { + return Promise.resolve(this._fsProvider.get(handle).delete(URI.revive(resource), opts)); } - $rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts)); + $rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { + return Promise.resolve(this._fsProvider.get(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts)); } - $copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).copy(URI.revive(oldUri), URI.revive(newUri), opts)); + $copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { + return Promise.resolve(this._fsProvider.get(handle).copy(URI.revive(oldUri), URI.revive(newUri), opts)); } - $mkdir(handle: number, resource: UriComponents): TPromise { - return asWinJsPromise(() => this._fsProvider.get(handle).createDirectory(URI.revive(resource))); + $mkdir(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._fsProvider.get(handle).createDirectory(URI.revive(resource))); } $watch(handle: number, session: number, resource: UriComponents, opts: files.IWatchOptions): void { - asWinJsPromise(() => { - let subscription = this._fsProvider.get(handle).watch(URI.revive(resource), opts); - this._watches.set(session, subscription); - }); + let subscription = this._fsProvider.get(handle).watch(URI.revive(resource), opts); + this._watches.set(session, subscription); } $unwatch(session: number): void { @@ -195,4 +200,21 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._watches.delete(session); } } + + $open(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._fsProvider.get(handle).open(URI.revive(resource))); + } + + $close(handle: number, fd: number): Promise { + return Promise.resolve(this._fsProvider.get(handle).close(fd)); + } + + $read(handle: number, fd: number, pos: number, data: Buffer, offset: number, length: number): Promise { + return Promise.resolve(this._fsProvider.get(handle).read(fd, pos, data, offset, length)); + } + + $write(handle: number, fd: number, pos: number, data: Buffer, offset: number, length: number): Promise { + return Promise.resolve(this._fsProvider.get(handle).write(fd, pos, data, offset, length)); + } + } diff --git a/src/vs/workbench/api/node/extHostFileSystemEventService.ts b/src/vs/workbench/api/node/extHostFileSystemEventService.ts index eeb7b256dcc..28e91aa10f3 100644 --- a/src/vs/workbench/api/node/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/node/extHostFileSystemEventService.ts @@ -4,18 +4,22 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from './extHostTypes'; -import { parse, IRelativePattern } from 'vs/base/common/glob'; -import { Uri, FileSystemWatcher as _FileSystemWatcher } from 'vscode'; -import { FileSystemEvents, ExtHostFileSystemEventServiceShape } from './extHost.protocol'; -import URI from 'vs/base/common/uri'; +import { flatten } from 'vs/base/common/arrays'; +import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; +import { IRelativePattern, parse } from 'vs/base/common/glob'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, ResourceFileEditDto, ResourceTextEditDto, MainThreadTextEditorsShape } from './extHost.protocol'; +import * as typeConverter from './extHostTypeConverters'; +import { Disposable, WorkspaceEdit } from './extHostTypes'; -class FileSystemWatcher implements _FileSystemWatcher { +class FileSystemWatcher implements vscode.FileSystemWatcher { - private _onDidCreate = new Emitter(); - private _onDidChange = new Emitter(); - private _onDidDelete = new Emitter(); + private _onDidCreate = new Emitter(); + private _onDidChange = new Emitter(); + private _onDidDelete = new Emitter(); private _disposable: Disposable; private _config: number; @@ -80,31 +84,100 @@ class FileSystemWatcher implements _FileSystemWatcher { this._disposable.dispose(); } - get onDidCreate(): Event { + get onDidCreate(): Event { return this._onDidCreate.event; } - get onDidChange(): Event { + get onDidChange(): Event { return this._onDidChange.event; } - get onDidDelete(): Event { + get onDidDelete(): Event { return this._onDidDelete.event; } } +interface WillRenameListener { + extension: IExtensionDescription; + (e: vscode.FileWillRenameEvent): any; +} + export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape { - private _emitter = new Emitter(); + private readonly _onFileEvent = new Emitter(); + private readonly _onDidRenameFile = new Emitter(); + private readonly _onWillRenameFile = new AsyncEmitter(); - constructor() { + readonly onDidRenameFile: Event = this._onDidRenameFile.event; + + constructor( + mainContext: IMainContext, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _mainThreadTextEditors: MainThreadTextEditorsShape = mainContext.getProxy(MainContext.MainThreadTextEditors) + ) { + // } - public createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): _FileSystemWatcher { - return new FileSystemWatcher(this._emitter.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); + public createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._onFileEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); } $onFileEvent(events: FileSystemEvents) { - this._emitter.fire(events); + this._onFileEvent.fire(events); + } + + $onFileRename(oldUri: UriComponents, newUri: UriComponents) { + this._onDidRenameFile.fire(Object.freeze({ oldUri: URI.revive(oldUri), newUri: URI.revive(newUri) })); + } + + getOnWillRenameFileEvent(extension: IExtensionDescription): Event { + return (listener, thisArg, disposables) => { + let wrappedListener = function () { + listener.apply(thisArg, arguments); + }; + wrappedListener.extension = extension; + return this._onWillRenameFile.event(wrappedListener, undefined, disposables); + }; + } + + $onWillRename(oldUriDto: UriComponents, newUriDto: UriComponents): Thenable { + const oldUri = URI.revive(oldUriDto); + const newUri = URI.revive(newUriDto); + + const edits: WorkspaceEdit[] = []; + return Promise.resolve(this._onWillRenameFile.fireAsync((bucket, _listener) => { + return { + oldUri, + newUri, + waitUntil: (thenable: Thenable): void => { + if (Object.isFrozen(bucket)) { + throw new TypeError('waitUntil cannot be called async'); + } + const index = bucket.length; + const wrappedThenable = Promise.resolve(thenable).then(result => { + // ignore all results except for WorkspaceEdits. Those + // are stored in a spare array + if (result instanceof WorkspaceEdit) { + edits[index] = result; + } + }); + bucket.push(wrappedThenable); + } + }; + }).then(() => { + if (edits.length === 0) { + return undefined; + } + // flatten all WorkspaceEdits collected via waitUntil-call + // and apply them in one go. + let allEdits = new Array<(ResourceFileEditDto | ResourceTextEditDto)[]>(); + for (let edit of edits) { + if (edit) { // sparse array + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + allEdits.push(edits); + } + } + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); + })); } } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 65a841ddd8b..fa47bf0e59e 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -4,26 +4,29 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/node/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, SymbolInformation2 } from 'vs/workbench/api/node/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol } from 'vs/workbench/api/node/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, SymbolInformationDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter } from './extHost.protocol'; +import { asThenable } from 'vs/base/common/async'; +import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto } from './extHost.protocol'; import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { isObject } from 'vs/base/common/types'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; // --- adapter @@ -37,21 +40,21 @@ class OutlineAdapter { this._provider = provider; } - provideDocumentSymbols(resource: URI): TPromise { + provideDocumentSymbols(resource: URI, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideDocumentSymbols(doc, token)).then(value => { + return asThenable(() => this._provider.provideDocumentSymbols(doc, token)).then(value => { if (isFalsyOrEmpty(value)) { return undefined; } - let [probe] = value; - if (!(probe instanceof SymbolInformation2)) { - value = OutlineAdapter._asSymbolHierarchy(resource, value); + if (value[0] instanceof DocumentSymbol) { + return (value).map(typeConvert.DocumentSymbol.from); + } else { + return OutlineAdapter._asDocumentSymbolTree(resource, value); } - return (value).map(typeConvert.SymbolInformation2.from); }); } - private static _asSymbolHierarchy(resource: URI, info: SymbolInformation[]): vscode.SymbolInformation2[] { + private static _asDocumentSymbolTree(resource: URI, info: SymbolInformation[]): modes.DocumentSymbol[] { // first sort by start (and end) and then loop over all elements // and build a tree based on containment. info = info.slice(0).sort((a, b) => { @@ -61,13 +64,17 @@ class OutlineAdapter { } return res; }); - let res: SymbolInformation2[] = []; - let parentStack: SymbolInformation2[] = []; + let res: modes.DocumentSymbol[] = []; + let parentStack: modes.DocumentSymbol[] = []; for (let i = 0; i < info.length; i++) { - let element = new SymbolInformation2(info[i].name, info[i].kind, '', info[i].location); - element.containerName = info[i].containerName; - element.location = info[i].location; // todo@joh make this proper - element.location.uri = element.location.uri || resource; + let element = { + name: info[i].name, + kind: typeConvert.SymbolKind.from(info[i].kind), + containerName: info[i].containerName, + range: typeConvert.Range.from(info[i].location.range), + selectionRange: typeConvert.Range.from(info[i].location.range), + children: [] + }; while (true) { if (parentStack.length === 0) { @@ -76,7 +83,7 @@ class OutlineAdapter { break; } let parent = parentStack[parentStack.length - 1]; - if (parent.definingRange.contains(element.definingRange) && !parent.definingRange.isEqual(element.definingRange)) { + if (EditorRange.containsRange(parent.range, element.range) && !EditorRange.equalsRange(parent.range, element.range)) { parent.children.push(element); parentStack.push(element); break; @@ -99,10 +106,10 @@ class CodeLensAdapter { private readonly _provider: vscode.CodeLensProvider ) { } - provideCodeLenses(resource: URI): TPromise { + provideCodeLenses(resource: URI, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideCodeLenses(doc, token)).then(lenses => { + return asThenable(() => this._provider.provideCodeLenses(doc, token)).then(lenses => { if (Array.isArray(lenses)) { return lenses.map(lens => { const id = this._heapService.keep(lens); @@ -116,18 +123,18 @@ class CodeLensAdapter { }); } - resolveCodeLens(resource: URI, symbol: modes.ICodeLensSymbol): TPromise { + resolveCodeLens(resource: URI, symbol: modes.ICodeLensSymbol, token: CancellationToken): Thenable { const lens = this._heapService.get(ObjectIdentifier.of(symbol)); if (!lens) { return undefined; } - let resolve: TPromise; + let resolve: Thenable; if (typeof this._provider.resolveCodeLens !== 'function' || lens.isResolved) { resolve = TPromise.as(lens); } else { - resolve = asWinJsPromise(token => this._provider.resolveCodeLens(lens, token)); + resolve = asThenable(() => this._provider.resolveCodeLens(lens, token)); } return resolve.then(newLens => { @@ -138,6 +145,15 @@ class CodeLensAdapter { } } +function convertToDefinitionLinks(value: vscode.Definition): modes.DefinitionLink[] { + if (Array.isArray(value)) { + return (value as (vscode.DefinitionLink | vscode.Location)[]).map(typeConvert.DefinitionLink.from); + } else if (value) { + return [typeConvert.DefinitionLink.from(value)]; + } + return undefined; +} + class DefinitionAdapter { constructor( @@ -145,17 +161,10 @@ class DefinitionAdapter { private readonly _provider: vscode.DefinitionProvider ) { } - provideDefinition(resource: URI, position: IPosition): TPromise { + provideDefinition(resource: URI, position: IPosition, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideDefinition(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return value.map(typeConvert.location.from); - } else if (value) { - return typeConvert.location.from(value); - } - return undefined; - }); + return asThenable(() => this._provider.provideDefinition(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -166,17 +175,10 @@ class ImplementationAdapter { private readonly _provider: vscode.ImplementationProvider ) { } - provideImplementation(resource: URI, position: IPosition): TPromise { + provideImplementation(resource: URI, position: IPosition, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideImplementation(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return value.map(typeConvert.location.from); - } else if (value) { - return typeConvert.location.from(value); - } - return undefined; - }); + return asThenable(() => this._provider.provideImplementation(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -187,17 +189,10 @@ class TypeDefinitionAdapter { private readonly _provider: vscode.TypeDefinitionProvider ) { } - provideTypeDefinition(resource: URI, position: IPosition): TPromise { + provideTypeDefinition(resource: URI, position: IPosition, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; const pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideTypeDefinition(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return value.map(typeConvert.location.from); - } else if (value) { - return typeConvert.location.from(value); - } - return undefined; - }); + return asThenable(() => this._provider.provideTypeDefinition(doc, pos, token)).then(convertToDefinitionLinks); } } @@ -208,12 +203,12 @@ class HoverAdapter { private readonly _provider: vscode.HoverProvider, ) { } - public provideHover(resource: URI, position: IPosition): TPromise { + public provideHover(resource: URI, position: IPosition, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideHover(doc, pos, token)).then(value => { + return asThenable(() => this._provider.provideHover(doc, pos, token)).then(value => { if (!value || isFalsyOrEmpty(value.contents)) { return undefined; } @@ -236,12 +231,12 @@ class DocumentHighlightAdapter { private readonly _provider: vscode.DocumentHighlightProvider ) { } - provideDocumentHighlights(resource: URI, position: IPosition): TPromise { + provideDocumentHighlights(resource: URI, position: IPosition, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideDocumentHighlights(doc, pos, token)).then(value => { + return asThenable(() => this._provider.provideDocumentHighlights(doc, pos, token)).then(value => { if (Array.isArray(value)) { return value.map(typeConvert.DocumentHighlight.from); } @@ -257,11 +252,11 @@ class ReferenceAdapter { private readonly _provider: vscode.ReferenceProvider ) { } - provideReferences(resource: URI, position: IPosition, context: modes.ReferenceContext): TPromise { + provideReferences(resource: URI, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideReferences(doc, pos, context, token)).then(value => { + return asThenable(() => this._provider.provideReferences(doc, pos, context, token)).then(value => { if (Array.isArray(value)) { return value.map(typeConvert.location.from); } @@ -280,10 +275,12 @@ class CodeActionAdapter { private readonly _documents: ExtHostDocuments, private readonly _commands: CommandsConverter, private readonly _diagnostics: ExtHostDiagnostics, - private readonly _provider: vscode.CodeActionProvider + private readonly _provider: vscode.CodeActionProvider, + private readonly _logService: ILogService, + private readonly _extensionId: string ) { } - provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext): TPromise { + provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; const ran = Selection.isISelection(rangeOrSelection) @@ -299,13 +296,10 @@ class CodeActionAdapter { const codeActionContext: vscode.CodeActionContext = { diagnostics: allDiagnostics, - only: context.only ? new CodeActionKind(context.only) : undefined, - triggerKind: context.trigger, + only: context.only ? new CodeActionKind(context.only) : undefined }; - return asWinJsPromise(token => - this._provider.provideCodeActions(doc, ran, codeActionContext, token) - ).then(commandsOrActions => { + return asThenable(() => this._provider.provideCodeActions(doc, ran, codeActionContext, token)).then(commandsOrActions => { if (isFalsyOrEmpty(commandsOrActions)) { return undefined; } @@ -322,6 +316,14 @@ class CodeActionAdapter { command: this._commands.toInternal(candidate), }); } else { + if (codeActionContext.only) { + if (!candidate.kind) { + this._logService.warn(`${this._extensionId} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); + } else if (!codeActionContext.only.contains(candidate.kind)) { + this._logService.warn(`${this._extensionId} -Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + } + } + // new school: convert code action result.push({ title: candidate.title, @@ -349,11 +351,11 @@ class DocumentFormattingAdapter { private readonly _provider: vscode.DocumentFormattingEditProvider ) { } - provideDocumentFormattingEdits(resource: URI, options: modes.FormattingOptions): TPromise { + provideDocumentFormattingEdits(resource: URI, options: modes.FormattingOptions, token: CancellationToken): Thenable { const { document } = this._documents.getDocumentData(resource); - return asWinJsPromise(token => this._provider.provideDocumentFormattingEdits(document, options, token)).then(value => { + return asThenable(() => this._provider.provideDocumentFormattingEdits(document, options, token)).then(value => { if (Array.isArray(value)) { return value.map(typeConvert.TextEdit.from); } @@ -369,12 +371,12 @@ class RangeFormattingAdapter { private readonly _provider: vscode.DocumentRangeFormattingEditProvider ) { } - provideDocumentRangeFormattingEdits(resource: URI, range: IRange, options: modes.FormattingOptions): TPromise { + provideDocumentRangeFormattingEdits(resource: URI, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Thenable { const { document } = this._documents.getDocumentData(resource); const ran = typeConvert.Range.to(range); - return asWinJsPromise(token => this._provider.provideDocumentRangeFormattingEdits(document, ran, options, token)).then(value => { + return asThenable(() => this._provider.provideDocumentRangeFormattingEdits(document, ran, options, token)).then(value => { if (Array.isArray(value)) { return value.map(typeConvert.TextEdit.from); } @@ -392,12 +394,12 @@ class OnTypeFormattingAdapter { autoFormatTriggerCharacters: string[] = []; // not here - provideOnTypeFormattingEdits(resource: URI, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise { + provideOnTypeFormattingEdits(resource: URI, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Thenable { const { document } = this._documents.getDocumentData(resource); const pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideOnTypeFormattingEdits(document, pos, ch, options, token)).then(value => { + return asThenable(() => this._provider.provideOnTypeFormattingEdits(document, pos, ch, options, token)).then(value => { if (Array.isArray(value)) { return value.map(typeConvert.TextEdit.from); } @@ -416,9 +418,9 @@ class NavigateTypeAdapter { this._provider = provider; } - provideWorkspaceSymbols(search: string): TPromise { + provideWorkspaceSymbols(search: string, token: CancellationToken): Thenable { const result: WorkspaceSymbolsDto = IdObject.mixin({ symbols: [] }); - return asWinJsPromise(token => this._provider.provideWorkspaceSymbols(search, token)).then(value => { + return asThenable(() => this._provider.provideWorkspaceSymbols(search, token)).then(value => { if (!isFalsyOrEmpty(value)) { for (const item of value) { if (!item) { @@ -429,7 +431,7 @@ class NavigateTypeAdapter { console.warn('INVALID SymbolInformation, lacks name', item); continue; } - const symbol = IdObject.mixin(typeConvert.SymbolInformation.from(item)); + const symbol = IdObject.mixin(typeConvert.WorkspaceSymbol.from(item)); this._symbolCache[symbol._id] = item; result.symbols.push(symbol); } @@ -442,7 +444,7 @@ class NavigateTypeAdapter { }); } - resolveWorkspaceSymbol(symbol: SymbolInformationDto): TPromise { + resolveWorkspaceSymbol(symbol: WorkspaceSymbolDto, token: CancellationToken): Thenable { if (typeof this._provider.resolveWorkspaceSymbol !== 'function') { return TPromise.as(symbol); @@ -450,8 +452,8 @@ class NavigateTypeAdapter { const item = this._symbolCache[symbol._id]; if (item) { - return asWinJsPromise(token => this._provider.resolveWorkspaceSymbol(item, token)).then(value => { - return value && mixin(symbol, typeConvert.SymbolInformation.from(value), true); + return asThenable(() => this._provider.resolveWorkspaceSymbol(item, token)).then(value => { + return value && mixin(symbol, typeConvert.WorkspaceSymbol.from(value), true); }); } return undefined; @@ -479,35 +481,28 @@ class RenameAdapter { private readonly _provider: vscode.RenameProvider ) { } - provideRenameEdits(resource: URI, position: IPosition, newName: string): TPromise { + provideRenameEdits(resource: URI, position: IPosition, newName: string, token: CancellationToken): Thenable { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideRenameEdits(doc, pos, newName, token)).then(value => { + return asThenable(() => this._provider.provideRenameEdits(doc, pos, newName, token)).then(value => { if (!value) { return undefined; } return typeConvert.WorkspaceEdit.from(value); }, err => { - if (typeof err === 'string') { - return { - edits: undefined, - rejectReason: err - }; - } else if (err instanceof Error && typeof err.message === 'string') { - return { - edits: undefined, - rejectReason: err.message - }; + let rejectReason = RenameAdapter._asMessage(err); + if (rejectReason) { + return { rejectReason, edits: undefined }; } else { // generic error - return TPromise.wrapError(err); + return Promise.reject(err); } }); } - resolveRenameLocation(resource: URI, position: IPosition): TPromise { + resolveRenameLocation(resource: URI, position: IPosition, token: CancellationToken): Thenable { if (typeof this._provider.prepareRename !== 'function') { return TPromise.as(undefined); } @@ -515,7 +510,7 @@ class RenameAdapter { let doc = this._documents.getDocumentData(resource).document; let pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.prepareRename(doc, pos, token)).then(rangeOrLocation => { + return asThenable(() => this._provider.prepareRename(doc, pos, token)).then(rangeOrLocation => { let range: vscode.Range; let text: string; @@ -537,8 +532,25 @@ class RenameAdapter { return undefined; } return { range: typeConvert.Range.from(range), text }; + }, err => { + let rejectReason = RenameAdapter._asMessage(err); + if (rejectReason) { + return { rejectReason, range: undefined, text: undefined }; + } else { + return Promise.reject(err); + } }); } + + private static _asMessage(err: any): string { + if (typeof err === 'string') { + return err; + } else if (err instanceof Error && typeof err.message === 'string') { + return err.message; + } else { + return undefined; + } + } } class SuggestAdapter { @@ -560,14 +572,14 @@ class SuggestAdapter { this._provider = provider; } - provideCompletionItems(resource: URI, position: IPosition, context: modes.SuggestContext): TPromise { + provideCompletionItems(resource: URI, position: IPosition, context: modes.SuggestContext, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; const pos = typeConvert.Position.to(position); - return asWinJsPromise(token => { - return this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.from(context)); - }).then(value => { + return asThenable( + () => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.from(context)) + ).then(value => { const _id = this._idPool++; @@ -607,7 +619,7 @@ class SuggestAdapter { }); } - resolveCompletionItem(resource: URI, position: IPosition, suggestion: modes.ISuggestion): TPromise { + resolveCompletionItem(resource: URI, position: IPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable { if (typeof this._provider.resolveCompletionItem !== 'function') { return TPromise.as(suggestion); @@ -619,7 +631,7 @@ class SuggestAdapter { return TPromise.as(suggestion); } - return asWinJsPromise(token => this._provider.resolveCompletionItem(item, token)).then(resolvedItem => { + return asThenable(() => this._provider.resolveCompletionItem(item, token)).then(resolvedItem => { if (!resolvedItem) { return suggestion; @@ -658,6 +670,7 @@ class SuggestAdapter { documentation: item.documentation, filterText: item.filterText, sortText: item.sortText, + preselect: item.preselect, // insertText: undefined, additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), @@ -711,12 +724,12 @@ class SignatureHelpAdapter { private readonly _provider: vscode.SignatureHelpProvider ) { } - provideSignatureHelp(resource: URI, position: IPosition): TPromise { + provideSignatureHelp(resource: URI, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; const pos = typeConvert.Position.to(position); - return asWinJsPromise(token => this._provider.provideSignatureHelp(doc, pos, token)).then(value => { + return asThenable(() => this._provider.provideSignatureHelp(doc, pos, token, context)).then(value => { if (value) { return typeConvert.SignatureHelp.from(value); } @@ -733,10 +746,10 @@ class LinkProviderAdapter { private readonly _provider: vscode.DocumentLinkProvider ) { } - provideLinks(resource: URI): TPromise { + provideLinks(resource: URI, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideDocumentLinks(doc, token)).then(links => { + return asThenable(() => this._provider.provideDocumentLinks(doc, token)).then(links => { if (!Array.isArray(links)) { return undefined; } @@ -751,7 +764,7 @@ class LinkProviderAdapter { }); } - resolveLink(link: modes.ILink): TPromise { + resolveLink(link: modes.ILink, token: CancellationToken): Thenable { if (typeof this._provider.resolveDocumentLink !== 'function') { return undefined; } @@ -762,7 +775,7 @@ class LinkProviderAdapter { return undefined; } - return asWinJsPromise(token => this._provider.resolveDocumentLink(item, token)).then(value => { + return asThenable(() => this._provider.resolveDocumentLink(item, token)).then(value => { if (value) { return typeConvert.DocumentLink.from(value); } @@ -778,9 +791,9 @@ class ColorProviderAdapter { private _provider: vscode.DocumentColorProvider ) { } - provideColors(resource: URI): TPromise { + provideColors(resource: URI, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideDocumentColors(doc, token)).then(colors => { + return asThenable(() => this._provider.provideDocumentColors(doc, token)).then(colors => { if (!Array.isArray(colors)) { return []; } @@ -796,11 +809,11 @@ class ColorProviderAdapter { }); } - provideColorPresentations(resource: URI, raw: IRawColorInfo): TPromise { + provideColorPresentations(resource: URI, raw: IRawColorInfo, token: CancellationToken): Thenable { const document = this._documents.getDocumentData(resource).document; const range = typeConvert.Range.to(raw.range); const color = typeConvert.Color.to(raw.color); - return asWinJsPromise(token => this._provider.provideColorPresentations(color, { document, range }, token)).then(value => { + return asThenable(() => this._provider.provideColorPresentations(color, { document, range }, token)).then(value => { return value.map(typeConvert.ColorPresentation.from); }); } @@ -813,9 +826,9 @@ class FoldingProviderAdapter { private _provider: vscode.FoldingRangeProvider ) { } - provideFoldingRanges(resource: URI, context: modes.FoldingContext): TPromise { + provideFoldingRanges(resource: URI, context: modes.FoldingContext, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; - return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, context, token)).then(ranges => { + return asThenable(() => this._provider.provideFoldingRanges(doc, context, token)).then(ranges => { if (!Array.isArray(ranges)) { return void 0; } @@ -845,6 +858,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { private _heapService: ExtHostHeapService; private _diagnostics: ExtHostDiagnostics; private _adapter = new Map(); + private readonly _logService: ILogService; constructor( mainContext: IMainContext, @@ -852,7 +866,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { documents: ExtHostDocuments, commands: ExtHostCommands, heapMonitor: ExtHostHeapService, - diagnostics: ExtHostDiagnostics + diagnostics: ExtHostDiagnostics, + logService: ILogService ) { this._schemeTransformer = schemeTransformer; this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageFeatures); @@ -860,6 +875,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { this._commands = commands; this._heapService = heapMonitor; this._diagnostics = diagnostics; + this._logService = logService; } private _transformDocumentSelector(selector: vscode.DocumentSelector): ISerializedDocumentFilter[] { @@ -909,7 +925,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => TPromise): TPromise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => Thenable): Thenable { let adapter = this._adapter.get(handle); if (!(adapter instanceof ctor)) { return TPromise.wrapError(new Error('no adapter found')); @@ -925,14 +941,14 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { // --- outline - registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, extensionId?: string): vscode.Disposable { + registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, extension?: IExtensionDescription): vscode.Disposable { const handle = this._addNewAdapter(new OutlineAdapter(this._documents, provider)); - this._proxy.$registerOutlineSupport(handle, this._transformDocumentSelector(selector), extensionId); + this._proxy.$registerOutlineSupport(handle, this._transformDocumentSelector(selector), extension ? extension.displayName || extension.name : undefined); return this._createDisposable(handle); } - $provideDocumentSymbols(handle: number, resource: UriComponents): TPromise { - return this._withAdapter(handle, OutlineAdapter, adapter => adapter.provideDocumentSymbols(URI.revive(resource))); + $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Thenable { + return this._withAdapter(handle, OutlineAdapter, adapter => adapter.provideDocumentSymbols(URI.revive(resource), token)); } // --- code lens @@ -953,12 +969,12 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return result; } - $provideCodeLenses(handle: number, resource: UriComponents): TPromise { - return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource))); + $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Thenable { + return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token)); } - $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol): TPromise { - return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol)); + $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol, token: CancellationToken): Thenable { + return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol, token)); } // --- declaration @@ -969,8 +985,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, DefinitionAdapter, adapter => adapter.provideDefinition(URI.revive(resource), position)); + $provideDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, DefinitionAdapter, adapter => adapter.provideDefinition(URI.revive(resource), position, token)); } registerImplementationProvider(selector: vscode.DocumentSelector, provider: vscode.ImplementationProvider): vscode.Disposable { @@ -979,8 +995,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideImplementation(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, ImplementationAdapter, adapter => adapter.provideImplementation(URI.revive(resource), position)); + $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, ImplementationAdapter, adapter => adapter.provideImplementation(URI.revive(resource), position, token)); } registerTypeDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.TypeDefinitionProvider): vscode.Disposable { @@ -989,8 +1005,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, TypeDefinitionAdapter, adapter => adapter.provideTypeDefinition(URI.revive(resource), position)); + $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, TypeDefinitionAdapter, adapter => adapter.provideTypeDefinition(URI.revive(resource), position, token)); } // --- extra info @@ -1001,8 +1017,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideHover(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, HoverAdapter, adpater => adpater.provideHover(URI.revive(resource), position)); + $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token)); } // --- occurrences @@ -1013,8 +1029,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, DocumentHighlightAdapter, adapter => adapter.provideDocumentHighlights(URI.revive(resource), position)); + $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, DocumentHighlightAdapter, adapter => adapter.provideDocumentHighlights(URI.revive(resource), position, token)); } // --- references @@ -1025,21 +1041,21 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext): TPromise { - return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context)); + $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token)); } // --- quick fix - registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { - const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider)); + registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, extension?: IExtensionDescription, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { + const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension ? extension.id : '')); this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), metadata && metadata.providedCodeActionKinds ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined); return this._createDisposable(handle); } - $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext): TPromise { - return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), rangeOrSelection, context)); + $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), rangeOrSelection, context, token)); } // --- formatting @@ -1050,8 +1066,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions): TPromise { - return this._withAdapter(handle, DocumentFormattingAdapter, adapter => adapter.provideDocumentFormattingEdits(URI.revive(resource), options)); + $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Thenable { + return this._withAdapter(handle, DocumentFormattingAdapter, adapter => adapter.provideDocumentFormattingEdits(URI.revive(resource), options, token)); } registerDocumentRangeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider): vscode.Disposable { @@ -1060,8 +1076,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions): TPromise { - return this._withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options)); + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Thenable { + return this._withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options, token)); } registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, triggerCharacters: string[]): vscode.Disposable { @@ -1070,8 +1086,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise { - return this._withAdapter(handle, OnTypeFormattingAdapter, adapter => adapter.provideOnTypeFormattingEdits(URI.revive(resource), position, ch, options)); + $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Thenable { + return this._withAdapter(handle, OnTypeFormattingAdapter, adapter => adapter.provideOnTypeFormattingEdits(URI.revive(resource), position, ch, options, token)); } // --- navigate types @@ -1082,15 +1098,15 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideWorkspaceSymbols(handle: number, search: string): TPromise { - return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.provideWorkspaceSymbols(search)); + $provideWorkspaceSymbols(handle: number, search: string, token: CancellationToken): Thenable { + return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.provideWorkspaceSymbols(search, token)); } - $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformationDto): TPromise { - return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.resolveWorkspaceSymbol(symbol)); + $resolveWorkspaceSymbol(handle: number, symbol: WorkspaceSymbolDto, token: CancellationToken): Thenable { + return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.resolveWorkspaceSymbol(symbol, token)); } - $releaseWorkspaceSymbols(handle: number, id: number) { + $releaseWorkspaceSymbols(handle: number, id: number): void { this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.releaseWorkspaceSymbols(id)); } @@ -1102,12 +1118,12 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string): TPromise { - return this._withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName)); + $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Thenable { + return this._withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName, token)); } - $resolveRenameLocation(handle: number, resource: URI, position: IPosition): TPromise { - return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position)); + $resolveRenameLocation(handle: number, resource: URI, position: IPosition, token: CancellationToken): Thenable { + return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveRenameLocation(URI.revive(resource), position, token)); } // --- suggestion @@ -1118,12 +1134,12 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext): TPromise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context)); + $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token)); } - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion): TPromise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, suggestion)); + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable { + return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, suggestion, token)); } $releaseCompletionItems(handle: number, id: number): void { @@ -1138,8 +1154,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition): TPromise { - return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position)); + $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, context, token)); } // --- links @@ -1150,12 +1166,12 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDocumentLinks(handle: number, resource: UriComponents): TPromise { - return this._withAdapter(handle, LinkProviderAdapter, adapter => adapter.provideLinks(URI.revive(resource))); + $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Thenable { + return this._withAdapter(handle, LinkProviderAdapter, adapter => adapter.provideLinks(URI.revive(resource), token)); } - $resolveDocumentLink(handle: number, link: modes.ILink): TPromise { - return this._withAdapter(handle, LinkProviderAdapter, adapter => adapter.resolveLink(link)); + $resolveDocumentLink(handle: number, link: modes.ILink, token: CancellationToken): Thenable { + return this._withAdapter(handle, LinkProviderAdapter, adapter => adapter.resolveLink(link, token)); } registerColorProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentColorProvider): vscode.Disposable { @@ -1164,12 +1180,12 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideDocumentColors(handle: number, resource: UriComponents): TPromise { - return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColors(URI.revive(resource))); + $provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): Thenable { + return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColors(URI.revive(resource), token)); } - $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo): TPromise { - return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo)); + $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo, token: CancellationToken): Thenable { + return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo, token)); } registerFoldingRangeProvider(selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider): vscode.Disposable { @@ -1178,8 +1194,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext): TPromise { - return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context)); + $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token)); } // --- configuration diff --git a/src/vs/workbench/api/node/extHostLanguages.ts b/src/vs/workbench/api/node/extHostLanguages.ts index 33baf762390..f8dac71a3b3 100644 --- a/src/vs/workbench/api/node/extHostLanguages.ts +++ b/src/vs/workbench/api/node/extHostLanguages.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { MainContext, MainThreadLanguagesShape, IMainContext } from './extHost.protocol'; +import * as vscode from 'vscode'; export class ExtHostLanguages { @@ -17,7 +17,10 @@ export class ExtHostLanguages { this._proxy = mainContext.getProxy(MainContext.MainThreadLanguages); } - getLanguages(): TPromise { + getLanguages(): Thenable { return this._proxy.$getLanguages(); } + changeLanguage(documentUri: vscode.Uri, languageId: string): Thenable { + return this._proxy.$changeLanguage(documentUri, languageId); + } } diff --git a/src/vs/workbench/api/node/extHostLogService.ts b/src/vs/workbench/api/node/extHostLogService.ts index 6708690a2f4..3d614625904 100644 --- a/src/vs/workbench/api/node/extHostLogService.ts +++ b/src/vs/workbench/api/node/extHostLogService.ts @@ -4,78 +4,34 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as vscode from 'vscode'; import { join } from 'vs/base/common/paths'; import { LogLevel } from 'vs/workbench/api/node/extHostTypes'; import { ILogService, DelegatedLogService } from 'vs/platform/log/common/log'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import { ExtHostLogServiceShape } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; export class ExtHostLogService extends DelegatedLogService implements ILogService, ExtHostLogServiceShape { - private _loggers: Map = new Map(); + private _logsPath: string; + readonly logFile: URI; constructor( - private _windowId: number, logLevel: LogLevel, - private _logsPath: string + logsPath: string, ) { - super(createSpdLogService(`exthost${_windowId}`, logLevel, _logsPath)); + super(createSpdLogService(ExtensionHostLogFileName, logLevel, logsPath)); + this._logsPath = logsPath; + this.logFile = URI.file(join(logsPath, `${ExtensionHostLogFileName}.log`)); } $setLevel(level: LogLevel): void { this.setLevel(level); } - getExtLogger(extensionID: string): ExtHostLogger { - let logger = this._loggers.get(extensionID); - if (!logger) { - logger = this.createLogger(extensionID); - this._loggers.set(extensionID, logger); - } - return logger; - } - getLogDirectory(extensionID: string): string { - return join(this._logsPath, `${extensionID}_${this._windowId}`); - } - - private createLogger(extensionID: string): ExtHostLogger { - const logsDirPath = this.getLogDirectory(extensionID); - const logService = createSpdLogService(extensionID, this.getLevel(), logsDirPath); - this._register(this.onDidChangeLogLevel(level => logService.setLevel(level))); - return new ExtHostLogger(logService); - } -} - -export class ExtHostLogger implements vscode.Logger { - - constructor( - private readonly _logService: ILogService - ) { } - - trace(message: string, ...args: any[]): void { - return this._logService.trace(message, ...args); - } - - debug(message: string, ...args: any[]): void { - return this._logService.debug(message, ...args); - } - - info(message: string, ...args: any[]): void { - return this._logService.info(message, ...args); - } - - warn(message: string, ...args: any[]): void { - return this._logService.warn(message, ...args); - } - - error(message: string | Error, ...args: any[]): void { - return this._logService.error(message, ...args); - } - - critical(message: string | Error, ...args: any[]): void { - return this._logService.critical(message, ...args); + return join(this._logsPath, extensionID); } } diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index b36f0bfb25e..1fd525bf8a4 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -6,38 +6,29 @@ import { MainContext, MainThreadOutputServiceShape, IMainContext } from './extHost.protocol'; import * as vscode from 'vscode'; +import { URI } from 'vs/base/common/uri'; +import { posix } from 'path'; +import { OutputAppender } from 'vs/platform/output/node/outputAppender'; +import { toLocalISOString } from 'vs/base/common/date'; -export class ExtHostOutputChannel implements vscode.OutputChannel { +export abstract class AbstractExtHostOutputChannel implements vscode.OutputChannel { - private static _idPool = 1; - - private _proxy: MainThreadOutputServiceShape; - private _name: string; - private _id: string; + protected readonly _id: Thenable; + private readonly _name: string; + protected readonly _proxy: MainThreadOutputServiceShape; private _disposed: boolean; - constructor(name: string, proxy: MainThreadOutputServiceShape) { + constructor(name: string, log: boolean, file: URI, proxy: MainThreadOutputServiceShape) { this._name = name; - this._id = 'extension-output-#' + (ExtHostOutputChannel._idPool++); this._proxy = proxy; + this._id = proxy.$register(this.name, log, file); } get name(): string { return this._name; } - dispose(): void { - if (!this._disposed) { - this._proxy.$dispose(this._id, this._name).then(() => { - this._disposed = true; - }); - } - } - - append(value: string): void { - this.validate(); - this._proxy.$append(this._id, this._name, value); - } + abstract append(value: string): void; appendLine(value: string): void { this.validate(); @@ -46,44 +37,113 @@ export class ExtHostOutputChannel implements vscode.OutputChannel { clear(): void { this.validate(); - this._proxy.$clear(this._id, this._name); + this._id.then(id => this._proxy.$clear(id)); } show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { this.validate(); - if (typeof columnOrPreserveFocus === 'boolean') { - preserveFocus = columnOrPreserveFocus; - } - - this._proxy.$reveal(this._id, this._name, preserveFocus); + this._id.then(id => this._proxy.$reveal(id, typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus)); } hide(): void { this.validate(); - this._proxy.$close(this._id); + this._id.then(id => this._proxy.$close(id)); } - private validate(): void { + protected validate(): void { if (this._disposed) { throw new Error('Channel has been closed'); } } + + dispose(): void { + if (!this._disposed) { + this._id + .then(id => this._proxy.$dispose(id)) + .then(() => this._disposed = true); + } + } +} + +export class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { + + constructor(name: string, proxy: MainThreadOutputServiceShape) { + super(name, false, null, proxy); + } + + append(value: string): void { + this.validate(); + this._id.then(id => this._proxy.$append(id, value)); + } +} + +export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { + + private static _namePool = 1; + private _appender: OutputAppender; + + constructor(name: string, outputDir: string, proxy: MainThreadOutputServiceShape) { + const fileName = `${ExtHostOutputChannelBackedByFile._namePool++}-${name}`; + const file = URI.file(posix.join(outputDir, `${fileName}.log`)); + + super(name, false, file, proxy); + this._appender = new OutputAppender(fileName, file.fsPath); + } + + append(value: string): void { + this.validate(); + this._appender.append(value); + } +} + +export class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { + + constructor(name: string, file: URI, proxy: MainThreadOutputServiceShape) { + super(name, true, file, proxy); + } + + append(value: string): void { + throw new Error('Not supported'); + } } export class ExtHostOutputService { private _proxy: MainThreadOutputServiceShape; + private _outputDir: string; - constructor(mainContext: IMainContext) { + constructor(logsLocation: URI, mainContext: IMainContext) { + this._outputDir = posix.join(logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService); } - createOutputChannel(name: string): vscode.OutputChannel { + createOutputChannel(name: string, push: boolean): vscode.OutputChannel { name = name.trim(); if (!name) { throw new Error('illegal argument `name`. must not be falsy'); } else { - return new ExtHostOutputChannel(name, this._proxy); + if (push) { + return new ExtHostPushOutputChannel(name, this._proxy); + } else { + // Do not crash if logger cannot be created + try { + return new ExtHostOutputChannelBackedByFile(name, this._outputDir, this._proxy); + } catch (error) { + console.log(error); + return new ExtHostPushOutputChannel(name, this._proxy); + } + } } } + + createOutputChannelFromLogFile(name: string, file: URI): vscode.OutputChannel { + name = name.trim(); + if (!name) { + throw new Error('illegal argument `name`. must not be falsy'); + } + if (!file) { + throw new Error('illegal argument `file`. must not be falsy'); + } + return new ExtHostLogFileOutputChannel(name, file, this._proxy); + } } diff --git a/src/vs/workbench/api/node/extHostProgress.ts b/src/vs/workbench/api/node/extHostProgress.ts index a9f8427ef0a..0700313c48c 100644 --- a/src/vs/workbench/api/node/extHostProgress.ts +++ b/src/vs/workbench/api/node/extHostProgress.ts @@ -8,10 +8,11 @@ import { ProgressOptions } from 'vscode'; import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol'; import { ProgressLocation } from './extHostTypeConverters'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { IProgressStep, Progress } from 'vs/platform/progress/common/progress'; +import { Progress } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { debounce } from 'vs/base/common/decorators'; +import { IProgressStep } from 'vs/workbench/services/progress/common/progress'; export class ExtHostProgress implements ExtHostProgressShape { @@ -70,11 +71,14 @@ export class ExtHostProgress implements ExtHostProgressShape { function mergeProgress(result: IProgressStep, currentValue: IProgressStep): IProgressStep { result.message = currentValue.message; - if (typeof currentValue.increment === 'number' && typeof result.message === 'number') { - result.increment += currentValue.increment; - } else if (typeof currentValue.increment === 'number') { - result.increment = currentValue.increment; + if (typeof currentValue.increment === 'number') { + if (typeof result.increment === 'number') { + result.increment += currentValue.increment; + } else { + result.increment = currentValue.increment; + } } + return result; } diff --git a/src/vs/workbench/api/node/extHostQuickOpen.ts b/src/vs/workbench/api/node/extHostQuickOpen.ts index bb1af794ce9..c58075883bf 100644 --- a/src/vs/workbench/api/node/extHostQuickOpen.ts +++ b/src/vs/workbench/api/node/extHostQuickOpen.ts @@ -4,13 +4,18 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { wireCancellationToken, asWinJsPromise } from 'vs/base/common/async'; +import { asThenable } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { QuickPickOptions, QuickPickItem, InputBoxOptions, WorkspaceFolderPickOptions, WorkspaceFolder } from 'vscode'; -import { MainContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, IMainContext } from './extHost.protocol'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { Emitter } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { InputBox, InputBoxOptions, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { ExtHostQuickOpenShape, IMainContext, MainContext, MainThreadQuickOpenShape, TransferQuickPickItems, TransferQuickInput, TransferQuickInputButton } from './extHost.protocol'; +import { URI } from 'vs/base/common/uri'; +import { ThemeIcon, QuickInputButtons } from 'vs/workbench/api/node/extHostTypes'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; export type Item = string | QuickPickItem; @@ -23,38 +28,40 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { private _onDidSelectItem: (handle: number) => void; private _validateInput: (input: string) => string | Thenable; + private _sessions = new Map(); + constructor(mainContext: IMainContext, workspace: ExtHostWorkspace, commands: ExtHostCommands) { this._proxy = mainContext.getProxy(MainContext.MainThreadQuickOpen); this._workspace = workspace; this._commands = commands; } - showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: QuickPickItem[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; - showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; - showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: QuickPickItem[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; - showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable { + showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + showQuickPick(itemsOrItemsPromise: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + showQuickPick(itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable { // clear state from last invocation this._onDidSelectItem = undefined; const itemsPromise = >TPromise.wrap(itemsOrItemsPromise); - const quickPickWidget = this._proxy.$show(multiStepHandle, { + const quickPickWidget = this._proxy.$show({ placeHolder: options && options.placeHolder, matchOnDescription: options && options.matchOnDescription, matchOnDetail: options && options.matchOnDetail, ignoreFocusLost: options && options.ignoreFocusOut, canPickMany: options && options.canPickMany - }); + }, token); - const promise = TPromise.any([]>[quickPickWidget, itemsPromise]).then(values => { + return TPromise.any([]>[quickPickWidget, itemsPromise]).then(values => { if (values.key === '0') { return undefined; } return itemsPromise.then(items => { - let pickItems: MyQuickPickItems[] = []; + let pickItems: TransferQuickPickItems[] = []; for (let handle = 0; handle < items.length; handle++) { let item = items[handle]; @@ -98,13 +105,16 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { } return undefined; }); - }, (err) => { - this._proxy.$setError(err); - - return TPromise.wrapError(err); }); + }).then(null, err => { + if (isPromiseCanceledError(err)) { + return undefined; + } + + this._proxy.$setError(err); + + return TPromise.wrapError(err); }); - return wireCancellationToken(token, promise, true); } $onItemSelected(handle: number): void { @@ -115,18 +125,26 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { // ---- input - showInput(multiStepHandle: number | undefined, options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable { + showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable { // global validate fn used in callback below this._validateInput = options && options.validateInput; - const promise = this._proxy.$input(multiStepHandle, options, typeof this._validateInput === 'function'); - return wireCancellationToken(token, promise, true); + return this._proxy.$input(options, typeof this._validateInput === 'function', token) + .then(null, err => { + if (isPromiseCanceledError(err)) { + return undefined; + } + + this._proxy.$setError(err); + + return TPromise.wrapError(err); + }); } - $validateInput(input: string): TPromise { + $validateInput(input: string): Thenable { if (this._validateInput) { - return asWinJsPromise(_ => this._validateInput(input)); + return asThenable(() => this._validateInput(input)); } return undefined; } @@ -142,4 +160,443 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape { return this._workspace.getWorkspaceFolders().filter(folder => folder.uri.toString() === selectedFolder.uri.toString())[0]; }); } + + // ---- QuickInput + + createQuickPick(extensionId: string): QuickPick { + const session = new ExtHostQuickPick(this._proxy, extensionId, () => this._sessions.delete(session._id)); + this._sessions.set(session._id, session); + return session; + } + + createInputBox(extensionId: string): InputBox { + const session = new ExtHostInputBox(this._proxy, extensionId, () => this._sessions.delete(session._id)); + this._sessions.set(session._id, session); + return session; + } + + $onDidChangeValue(sessionId: number, value: string): void { + const session = this._sessions.get(sessionId); + if (session) { + session._fireDidChangeValue(value); + } + } + + $onDidAccept(sessionId: number): void { + const session = this._sessions.get(sessionId); + if (session) { + session._fireDidAccept(); + } + } + + $onDidChangeActive(sessionId: number, handles: number[]): void { + const session = this._sessions.get(sessionId); + if (session instanceof ExtHostQuickPick) { + session._fireDidChangeActive(handles); + } + } + + $onDidChangeSelection(sessionId: number, handles: number[]): void { + const session = this._sessions.get(sessionId); + if (session instanceof ExtHostQuickPick) { + session._fireDidChangeSelection(handles); + } + } + + $onDidTriggerButton(sessionId: number, handle: number): void { + const session = this._sessions.get(sessionId); + if (session) { + session._fireDidTriggerButton(handle); + } + } + + $onDidHide(sessionId: number): void { + const session = this._sessions.get(sessionId); + if (session) { + session._fireDidHide(); + } + } +} + +class ExtHostQuickInput implements QuickInput { + + private static _nextId = 1; + _id = ExtHostQuickPick._nextId++; + + private _title: string; + private _steps: number; + private _totalSteps: number; + private _visible = false; + private _enabled = true; + private _busy = false; + private _ignoreFocusOut = true; + private _value = ''; + private _placeholder: string; + private _buttons: QuickInputButton[] = []; + private _handlesToButtons = new Map(); + private _onDidAcceptEmitter = new Emitter(); + private _onDidChangeValueEmitter = new Emitter(); + private _onDidTriggerButtonEmitter = new Emitter(); + private _onDidHideEmitter = new Emitter(); + private _updateTimeout: number; + private _pendingUpdate: TransferQuickInput = { id: this._id }; + + private _disposed = false; + protected _disposables: IDisposable[] = [ + this._onDidTriggerButtonEmitter, + this._onDidHideEmitter, + this._onDidAcceptEmitter, + this._onDidChangeValueEmitter + ]; + + constructor(protected _proxy: MainThreadQuickOpenShape, protected _extensionId: string, private _onDidDispose: () => void) { + } + + get title() { + return this._title; + } + + set title(title: string) { + this._title = title; + this.update({ title }); + } + + get step() { + return this._steps; + } + + set step(step: number) { + this._steps = step; + this.update({ step }); + } + + get totalSteps() { + return this._totalSteps; + } + + set totalSteps(totalSteps: number) { + this._totalSteps = totalSteps; + this.update({ totalSteps }); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled: boolean) { + this._enabled = enabled; + this.update({ enabled }); + } + + get busy() { + return this._busy; + } + + set busy(busy: boolean) { + this._busy = busy; + this.update({ busy }); + } + + get ignoreFocusOut() { + return this._ignoreFocusOut; + } + + set ignoreFocusOut(ignoreFocusOut: boolean) { + this._ignoreFocusOut = ignoreFocusOut; + this.update({ ignoreFocusOut }); + } + + get value() { + return this._value; + } + + set value(value: string) { + this._value = value; + this.update({ value }); + } + + get placeholder() { + return this._placeholder; + } + + set placeholder(placeholder: string) { + this._placeholder = placeholder; + this.update({ placeholder }); + } + + onDidChangeValue = this._onDidChangeValueEmitter.event; + + onDidAccept = this._onDidAcceptEmitter.event; + + get buttons() { + return this._buttons; + } + + set buttons(buttons: QuickInputButton[]) { + this._buttons = buttons.slice(); + this._handlesToButtons.clear(); + buttons.forEach((button, i) => { + const handle = button === QuickInputButtons.Back ? -1 : i; + this._handlesToButtons.set(handle, button); + }); + this.update({ + buttons: buttons.map((button, i) => ({ + iconPath: getIconUris(button.iconPath), + tooltip: button.tooltip, + handle: button === QuickInputButtons.Back ? -1 : i, + })) + }); + } + + onDidTriggerButton = this._onDidTriggerButtonEmitter.event; + + show(): void { + this._visible = true; + this.update({ visible: true }); + } + + hide(): void { + this._visible = false; + this.update({ visible: false }); + } + + onDidHide = this._onDidHideEmitter.event; + + _fireDidAccept() { + this._onDidAcceptEmitter.fire(); + } + + _fireDidChangeValue(value) { + this._value = value; + this._onDidChangeValueEmitter.fire(value); + } + + _fireDidTriggerButton(handle: number) { + const button = this._handlesToButtons.get(handle); + this._onDidTriggerButtonEmitter.fire(button); + } + + _fireDidHide() { + this._onDidHideEmitter.fire(); + } + + public dispose(): void { + if (this._disposed) { + return; + } + this._disposed = true; + this._fireDidHide(); + this._disposables = dispose(this._disposables); + if (this._updateTimeout) { + clearTimeout(this._updateTimeout); + this._updateTimeout = undefined; + } + this._onDidDispose(); + this._proxy.$dispose(this._id); + } + + protected update(properties: Record): void { + if (this._disposed) { + return; + } + for (const key of Object.keys(properties)) { + const value = properties[key]; + this._pendingUpdate[key] = value === undefined ? null : value; + } + + if ('visible' in this._pendingUpdate) { + if (this._updateTimeout) { + clearTimeout(this._updateTimeout); + this._updateTimeout = undefined; + } + this.dispatchUpdate(); + } else if (this._visible && !this._updateTimeout) { + // Defer the update so that multiple changes to setters dont cause a redraw each + this._updateTimeout = setTimeout(() => { + this._updateTimeout = undefined; + this.dispatchUpdate(); + }, 0); + } + } + + private dispatchUpdate() { + this._proxy.$createOrUpdate(this._pendingUpdate); + this._pendingUpdate = { id: this._id }; + } +} + +function getIconUris(iconPath: QuickInputButton['iconPath']) { + const light = getLightIconUri(iconPath); + return { dark: getDarkIconUri(iconPath) || light, light }; +} + +function getLightIconUri(iconPath: QuickInputButton['iconPath']) { + if (iconPath && !(iconPath instanceof ThemeIcon)) { + if (typeof iconPath === 'string' + || iconPath instanceof URI) { + return getIconUri(iconPath); + } + return getIconUri(iconPath['light']); + } + return undefined; +} + +function getDarkIconUri(iconPath: QuickInputButton['iconPath']) { + if (iconPath && !(iconPath instanceof ThemeIcon) && iconPath['dark']) { + return getIconUri(iconPath['dark']); + } + return undefined; +} + +function getIconUri(iconPath: string | URI) { + if (iconPath instanceof URI) { + return iconPath; + } + return URI.file(iconPath); +} + +class ExtHostQuickPick extends ExtHostQuickInput implements QuickPick { + + private _items: T[] = []; + private _handlesToItems = new Map(); + private _itemsToHandles = new Map(); + private _canSelectMany = false; + private _matchOnDescription = true; + private _matchOnDetail = true; + private _activeItems: T[] = []; + private _onDidChangeActiveEmitter = new Emitter(); + private _selectedItems: T[] = []; + private _onDidChangeSelectionEmitter = new Emitter(); + + constructor(proxy: MainThreadQuickOpenShape, extensionId: string, onDispose: () => void) { + super(proxy, extensionId, onDispose); + this._disposables.push( + this._onDidChangeActiveEmitter, + this._onDidChangeSelectionEmitter, + ); + this.update({ type: 'quickPick' }); + } + + get items() { + return this._items; + } + + set items(items: T[]) { + this._items = items.slice(); + this._handlesToItems.clear(); + this._itemsToHandles.clear(); + items.forEach((item, i) => { + this._handlesToItems.set(i, item); + this._itemsToHandles.set(item, i); + }); + this.update({ + items: items.map((item, i) => ({ + label: item.label, + description: item.description, + handle: i, + detail: item.detail, + picked: item.picked + })) + }); + } + + get canSelectMany() { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + this.update({ canSelectMany }); + } + + get matchOnDescription() { + return this._matchOnDescription; + } + + set matchOnDescription(matchOnDescription: boolean) { + this._matchOnDescription = matchOnDescription; + this.update({ matchOnDescription }); + } + + get matchOnDetail() { + return this._matchOnDetail; + } + + set matchOnDetail(matchOnDetail: boolean) { + this._matchOnDetail = matchOnDetail; + this.update({ matchOnDetail }); + } + + get activeItems() { + return this._activeItems; + } + + set activeItems(activeItems: T[]) { + this._activeItems = activeItems.filter(item => this._itemsToHandles.has(item)); + this.update({ activeItems: this._activeItems.map(item => this._itemsToHandles.get(item)) }); + } + + onDidChangeActive = this._onDidChangeActiveEmitter.event; + + get selectedItems() { + return this._selectedItems; + } + + set selectedItems(selectedItems: T[]) { + this._selectedItems = selectedItems.filter(item => this._itemsToHandles.has(item)); + this.update({ selectedItems: this._selectedItems.map(item => this._itemsToHandles.get(item)) }); + } + + onDidChangeSelection = this._onDidChangeSelectionEmitter.event; + + _fireDidChangeActive(handles: number[]) { + const items = handles.map(handle => this._handlesToItems.get(handle)); + this._activeItems = items; + this._onDidChangeActiveEmitter.fire(items); + } + + _fireDidChangeSelection(handles: number[]) { + const items = handles.map(handle => this._handlesToItems.get(handle)); + this._selectedItems = items; + this._onDidChangeSelectionEmitter.fire(items); + } +} + +class ExtHostInputBox extends ExtHostQuickInput implements InputBox { + + private _password: boolean; + private _prompt: string; + private _validationMessage: string; + + constructor(proxy: MainThreadQuickOpenShape, extensionId: string, onDispose: () => void) { + super(proxy, extensionId, onDispose); + this.update({ type: 'inputBox' }); + } + + get password() { + return this._password; + } + + set password(password: boolean) { + this._password = password; + this.update({ password }); + } + + get prompt() { + return this._prompt; + } + + set prompt(prompt: string) { + this._prompt = prompt; + this.update({ prompt }); + } + + get validationMessage() { + return this._validationMessage; + } + + set validationMessage(validationMessage: string) { + this._validationMessage = validationMessage; + this.update({ validationMessage }); + } } diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index b008b72b086..f1996d5ea4b 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter, once } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { asThenable } from 'vs/base/common/async'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape } from './extHost.protocol'; @@ -18,6 +18,7 @@ import { comparePaths } from 'vs/base/common/comparers'; import * as vscode from 'vscode'; import { ISplice } from 'vs/base/common/sequence'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; type ProviderHandle = number; type GroupHandle = number; @@ -237,14 +238,14 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG return this._resourceStatesMap.get(handle); } - async $executeResourceCommand(handle: number): TPromise { + $executeResourceCommand(handle: number): Thenable { const command = this._resourceStatesCommandsMap.get(handle); if (!command) { - return; + return Promise.resolve(null); } - await this._commands.executeCommand(command.command, ...command.arguments); + return asThenable(() => this._commands.executeCommand(command.command, ...command.arguments)); } _takeResourceStateSnapshot(): SCMRawResourceSplice[] { @@ -395,6 +396,15 @@ class ExtHostSourceControl implements vscode.SourceControl { this._proxy.$updateSourceControl(this.handle, { statusBarCommands: internal }); } + private _selected: boolean = false; + + get selected(): boolean { + return this._selected; + } + + private _onDidChangeSelection = new Emitter(); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private handle: number = ExtHostSourceControl._handlePool++; constructor( @@ -454,6 +464,11 @@ class ExtHostSourceControl implements vscode.SourceControl { return this._groups.get(handle); } + setSelectionState(selected: boolean): void { + this._selected = selected; + this._onDidChangeSelection.fire(selected); + } + dispose(): void { this._groups.forEach(group => group.dispose()); this._proxy.$unregisterSourceControl(this.handle); @@ -471,6 +486,8 @@ export class ExtHostSCM implements ExtHostSCMShape { private _onDidChangeActiveProvider = new Emitter(); get onDidChangeActiveProvider(): Event { return this._onDidChangeActiveProvider.event; } + private _selectedSourceControlHandles = new Set(); + constructor( mainContext: IMainContext, private _commands: ExtHostCommands, @@ -542,7 +559,7 @@ export class ExtHostSCM implements ExtHostSCMShape { return inputBox; } - $provideOriginalResource(sourceControlHandle: number, uriComponents: UriComponents): TPromise { + $provideOriginalResource(sourceControlHandle: number, uriComponents: UriComponents, token: CancellationToken): Thenable { const uri = URI.revive(uriComponents); this.logService.trace('ExtHostSCM#$provideOriginalResource', sourceControlHandle, uri.toString()); @@ -552,7 +569,7 @@ export class ExtHostSCM implements ExtHostSCMShape { return TPromise.as(null); } - return asWinJsPromise(token => sourceControl.quickDiffProvider.provideOriginalResource(uri, token)); + return asThenable(() => sourceControl.quickDiffProvider.provideOriginalResource(uri, token)); } $onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise { @@ -568,25 +585,25 @@ export class ExtHostSCM implements ExtHostSCMShape { return TPromise.as(null); } - async $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise { + $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): Thenable { this.logService.trace('ExtHostSCM#$executeResourceCommand', sourceControlHandle, groupHandle, handle); const sourceControl = this._sourceControls.get(sourceControlHandle); if (!sourceControl) { - return; + return TPromise.as(null); } const group = sourceControl.getResourceGroup(groupHandle); if (!group) { - return; + return TPromise.as(null); } - await group.$executeResourceCommand(handle); + return group.$executeResourceCommand(handle); } - async $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined> { + $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Thenable<[string, number] | undefined> { this.logService.trace('ExtHostSCM#$validateInput', sourceControlHandle); const sourceControl = this._sourceControls.get(sourceControlHandle); @@ -599,12 +616,49 @@ export class ExtHostSCM implements ExtHostSCMShape { return TPromise.as(undefined); } - const result = await sourceControl.inputBox.validateInput(value, cursorPosition); + return asThenable(() => sourceControl.inputBox.validateInput(value, cursorPosition)).then(result => { + if (!result) { + return TPromise.as(undefined); + } - if (!result) { - return TPromise.as(undefined); + return TPromise.as<[string, number]>([result.message, result.type]); + }); + } + + $setSelectedSourceControls(selectedSourceControlHandles: number[]): Thenable { + this.logService.trace('ExtHostSCM#$setSelectedSourceControls', selectedSourceControlHandles); + + const set = new Set(); + + for (const handle of selectedSourceControlHandles) { + set.add(handle); } - return [result.message, result.type]; + set.forEach(handle => { + if (!this._selectedSourceControlHandles.has(handle)) { + const sourceControl = this._sourceControls.get(handle); + + if (!sourceControl) { + return; + } + + sourceControl.setSelectionState(true); + } + }); + + this._selectedSourceControlHandles.forEach(handle => { + if (!set.has(handle)) { + const sourceControl = this._sourceControls.get(handle); + + if (!sourceControl) { + return; + } + + sourceControl.setSelectionState(false); + } + }); + + this._selectedSourceControlHandles = set; + return TPromise.as(null); } } diff --git a/src/vs/workbench/api/node/extHostSearch.fileIndex.ts b/src/vs/workbench/api/node/extHostSearch.fileIndex.ts new file mode 100644 index 00000000000..4cc7e4d150a --- /dev/null +++ b/src/vs/workbench/api/node/extHostSearch.fileIndex.ts @@ -0,0 +1,724 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as path from 'path'; +import * as arrays from 'vs/base/common/arrays'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { canceled } from 'vs/base/common/errors'; +import * as glob from 'vs/base/common/glob'; +import * as resources from 'vs/base/common/resources'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import * as strings from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { ICachedSearchStats, IFileIndexProviderStats, IFileMatch, IFileSearchStats, IFolderQuery, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } from 'vs/platform/search/common/search'; +import * as vscode from 'vscode'; + +export interface IInternalFileMatch { + base: URI; + original?: URI; + relativePath?: string; // Not present for extraFiles or absolute path matches + basename: string; + size?: number; +} + +/** + * Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns + */ +export function resolvePatternsForProvider(globalPattern: glob.IExpression, folderPattern: glob.IExpression): string[] { + const merged = { + ...(globalPattern || {}), + ...(folderPattern || {}) + }; + + return Object.keys(merged) + .filter(key => { + const value = merged[key]; + return typeof value === 'boolean' && value; + }); +} + +export class QueryGlobTester { + + private _excludeExpression: glob.IExpression; + private _parsedExcludeExpression: glob.ParsedExpression; + + private _parsedIncludeExpression: glob.ParsedExpression; + + constructor(config: ISearchQuery, folderQuery: IFolderQuery) { + this._excludeExpression = { + ...(config.excludePattern || {}), + ...(folderQuery.excludePattern || {}) + }; + this._parsedExcludeExpression = glob.parse(this._excludeExpression); + + // Empty includeExpression means include nothing, so no {} shortcuts + let includeExpression: glob.IExpression = config.includePattern; + if (folderQuery.includePattern) { + if (includeExpression) { + includeExpression = { + ...includeExpression, + ...folderQuery.includePattern + }; + } else { + includeExpression = folderQuery.includePattern; + } + } + + if (includeExpression) { + this._parsedIncludeExpression = glob.parse(includeExpression); + } + } + + /** + * Guaranteed sync - siblingsFn should not return a promise. + */ + public includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean { + if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) { + return false; + } + + if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) { + return false; + } + + return true; + } + + /** + * Guaranteed async. + */ + public includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): TPromise { + const excludeP = this._parsedExcludeExpression ? + TPromise.as(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) : + TPromise.wrap(false); + + return excludeP.then(excluded => { + if (excluded) { + return false; + } + + return this._parsedIncludeExpression ? + TPromise.as(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) : + TPromise.wrap(true); + }).then(included => { + return included; + }); + } + + public hasSiblingExcludeClauses(): boolean { + return hasSiblingClauses(this._excludeExpression); + } +} + +function hasSiblingClauses(pattern: glob.IExpression): boolean { + for (let key in pattern) { + if (typeof pattern[key] !== 'boolean') { + return true; + } + } + + return false; +} + +export interface IDirectoryEntry { + base: URI; + relativePath: string; + basename: string; +} + +export interface IDirectoryTree { + rootEntries: IDirectoryEntry[]; + pathToEntries: { [relativePath: string]: IDirectoryEntry[] }; +} + +interface IInternalSearchComplete { + limitHit: boolean; + results: IInternalFileMatch[]; + stats: T; +} + +export class FileIndexSearchEngine { + private filePattern: string; + private normalizedFilePatternLowercase: string; + private includePattern: glob.ParsedExpression; + private maxResults: number; + private exists: boolean; + private isLimitHit: boolean; + private resultCount: number; + private isCanceled: boolean; + + private filesWalked = 0; + private dirsWalked = 0; + + private activeCancellationTokens: Set; + + private globalExcludePattern: glob.ParsedExpression; + + constructor(private config: ISearchQuery, private provider: vscode.FileIndexProvider) { + this.filePattern = config.filePattern; + this.includePattern = config.includePattern && glob.parse(config.includePattern); + this.maxResults = config.maxResults || null; + this.exists = config.exists; + this.resultCount = 0; + this.isLimitHit = false; + this.activeCancellationTokens = new Set(); + + if (this.filePattern) { + this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); + } + + this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern); + } + + public cancel(): void { + this.isCanceled = true; + this.activeCancellationTokens.forEach(t => t.cancel()); + this.activeCancellationTokens = new Set(); + } + + public search(_onResult: (match: IInternalFileMatch) => void): TPromise<{ isLimitHit: boolean, stats: IFileIndexProviderStats }> { + if (this.config.folderQueries.length !== 1) { + throw new Error('Searches just one folder'); + } + + // Searches a single folder + const folderQuery = this.config.folderQueries[0]; + + return new TPromise<{ isLimitHit: boolean, stats: IFileIndexProviderStats }>((resolve, reject) => { + const onResult = (match: IInternalFileMatch) => { + this.resultCount++; + _onResult(match); + }; + + if (this.isCanceled) { + throw canceled(); + } + + // For each extra file + if (this.config.extraFileResources) { + this.config.extraFileResources + .forEach(extraFile => { + const extraFileStr = extraFile.toString(); // ? + const basename = path.basename(extraFileStr); + if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { + return; // excluded + } + + // File: Check for match on file pattern and include pattern + this.matchFile(onResult, { base: extraFile, basename }); + }); + } + + return this.searchInFolder(folderQuery, _onResult) + .then(stats => { + resolve({ + isLimitHit: this.isLimitHit, + stats + }); + }, (errs: Error[]) => { + const errMsg = errs + .map(err => toErrorMessage(err)) + .filter(msg => !!msg)[0]; + + reject(new Error(errMsg)); + }); + }); + } + + private searchInFolder(fq: IFolderQuery, onResult: (match: IInternalFileMatch) => void): TPromise { + let cancellation = new CancellationTokenSource(); + return new TPromise((resolve, reject) => { + const options = this.getSearchOptionsForFolder(fq); + const tree = this.initDirectoryTree(); + + const queryTester = new QueryGlobTester(this.config, fq); + const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses(); + + const onProviderResult = (uri: URI) => { + if (this.isCanceled) { + return; + } + + // TODO@rob - ??? + const relativePath = path.relative(fq.folder.path, uri.path); + if (noSiblingsClauses) { + const basename = path.basename(uri.path); + this.matchFile(onResult, { base: fq.folder, relativePath, basename, original: uri }); + + return; + } + + // TODO: Optimize siblings clauses with ripgrep here. + this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); + }; + + let providerSW: StopWatch; + let providerTime: number; + let fileWalkTime: number; + new TPromise(resolve => process.nextTick(resolve)) + .then(() => { + this.activeCancellationTokens.add(cancellation); + providerSW = StopWatch.create(); + return this.provider.provideFileIndex(options, cancellation.token); + }) + .then(results => { + providerTime = providerSW.elapsed(); + const postProcessSW = StopWatch.create(); + this.activeCancellationTokens.delete(cancellation); + if (this.isCanceled) { + return null; + } + + results.forEach(onProviderResult); + + this.matchDirectoryTree(tree, queryTester, onResult); + fileWalkTime = postProcessSW.elapsed(); + return null; + }).then( + () => { + cancellation.dispose(); + resolve({ + providerTime, + fileWalkTime, + directoriesWalked: this.dirsWalked, + filesWalked: this.filesWalked + }); + }, + err => { + cancellation.dispose(); + reject(err); + }); + }); + } + + private getSearchOptionsForFolder(fq: IFolderQuery): vscode.FileIndexOptions { + const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern); + const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern); + + return { + folder: fq.folder, + excludes, + includes, + useIgnoreFiles: !this.config.disregardIgnoreFiles, + followSymlinks: !this.config.ignoreSymlinks + }; + } + + private initDirectoryTree(): IDirectoryTree { + const tree: IDirectoryTree = { + rootEntries: [], + pathToEntries: Object.create(null) + }; + tree.pathToEntries['.'] = tree.rootEntries; + return tree; + } + + private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) { + // Support relative paths to files from a root resource (ignores excludes) + if (relativeFile === this.filePattern) { + const basename = path.basename(this.filePattern); + this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename }); + } + + function add(relativePath: string) { + const basename = path.basename(relativePath); + const dirname = path.dirname(relativePath); + let entries = pathToEntries[dirname]; + if (!entries) { + entries = pathToEntries[dirname] = []; + add(dirname); + } + entries.push({ + base, + relativePath, + basename + }); + } + + add(relativeFile); + } + + private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) { + const self = this; + const filePattern = this.filePattern; + function matchDirectory(entries: IDirectoryEntry[]) { + self.dirsWalked++; + for (let i = 0, n = entries.length; i < n; i++) { + const entry = entries[i]; + const { relativePath, basename } = entry; + + // Check exclude pattern + // If the user searches for the exact file name, we adjust the glob matching + // to ignore filtering by siblings because the user seems to know what she + // is searching for and we want to include the result in that case anyway + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); + if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { + continue; + } + + const sub = pathToEntries[relativePath]; + if (sub) { + matchDirectory(sub); + } else { + self.filesWalked++; + if (relativePath === filePattern) { + continue; // ignore file if its path matches with the file pattern because that is already matched above + } + + self.matchFile(onResult, entry); + } + + if (self.isLimitHit) { + break; + } + } + } + matchDirectory(rootEntries); + } + + private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void { + if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { + if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) { + this.isLimitHit = true; + this.cancel(); + } + + if (!this.isLimitHit) { + onResult(candidate); + } + } + } + + private isFilePatternMatch(path: string): boolean { + // Check for search pattern + if (this.filePattern) { + if (this.filePattern === '*') { + return true; // support the all-matching wildcard + } + + return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); + } + + // No patterns means we match all + return true; + } +} + +export class FileIndexSearchManager { + + private static readonly BATCH_SIZE = 512; + + private caches: { [cacheKey: string]: Cache; } = Object.create(null); + + private readonly folderCacheKeys = new Map>(); + + public fileSearch(config: ISearchQuery, provider: vscode.FileIndexProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): TPromise { + if (config.sortByScore) { + let sortedSearch = this.trySortedSearchFromCache(config, token); + if (!sortedSearch) { + const engineConfig = config.maxResults ? + { + ...config, + ...{ maxResults: null } + } : + config; + + const engine = new FileIndexSearchEngine(engineConfig, provider); + sortedSearch = this.doSortedSearch(engine, config, token); + } + + return sortedSearch.then(complete => { + this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE); + return complete; + }); + } + + const engine = new FileIndexSearchEngine(config, provider); + return this.doSearch(engine, token) + .then(complete => { + this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE); + return { + limitHit: complete.limitHit, + stats: { + type: 'fileIndexProvider', + detailStats: complete.stats, + fromCache: false, + resultCount: complete.results.length + } + }; + }); + } + + private getFolderCacheKey(config: ISearchQuery): string { + const uri = config.folderQueries[0].folder.toString(); + const folderCacheKey = config.cacheKey && `${uri}_${config.cacheKey}`; + if (!this.folderCacheKeys.get(config.cacheKey)) { + this.folderCacheKeys.set(config.cacheKey, new Set()); + } + + this.folderCacheKeys.get(config.cacheKey).add(folderCacheKey); + + return folderCacheKey; + } + + private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch { + return { + resource: match.original || resources.joinPath(match.base, match.relativePath) + }; + } + + private doSortedSearch(engine: FileIndexSearchEngine, config: ISearchQuery, token: CancellationToken): TPromise { + let allResultsPromise = createCancelablePromise>(token => { + return this.doSearch(engine, token); + }); + + const folderCacheKey = this.getFolderCacheKey(config); + let cache: Cache; + if (folderCacheKey) { + cache = this.getOrCreateCache(folderCacheKey); + const cacheRow: ICacheRow = { + promise: allResultsPromise, + resolved: false + }; + cache.resultsToSearchCache[config.filePattern] = cacheRow; + allResultsPromise.then(() => { + cacheRow.resolved = true; + }, err => { + delete cache.resultsToSearchCache[config.filePattern]; + }); + allResultsPromise = this.preventCancellation(allResultsPromise); + } + + return TPromise.wrap( + allResultsPromise.then(complete => { + const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); + const sortSW = (typeof config.maxResults !== 'number' || config.maxResults > 0) && StopWatch.create(); + return this.sortResults(config, complete.results, scorerCache, token) + .then(sortedResults => { + // sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickopen is opened. + // Contrasting with findFiles which is not sorted and will have sortingTime: undefined + const sortingTime = sortSW ? sortSW.elapsed() : -1; + return { + limitHit: complete.limitHit || typeof config.maxResults === 'number' && complete.results.length > config.maxResults, // ?? + results: sortedResults, + stats: { + detailStats: complete.stats, + fromCache: false, + resultCount: sortedResults.length, + sortingTime, + type: 'fileIndexProvider' + } + }; + }); + })); + } + + private getOrCreateCache(cacheKey: string): Cache { + const existing = this.caches[cacheKey]; + if (existing) { + return existing; + } + return this.caches[cacheKey] = new Cache(); + } + + private trySortedSearchFromCache(config: ISearchQuery, token: CancellationToken): TPromise { + const folderCacheKey = this.getFolderCacheKey(config); + const cache = folderCacheKey && this.caches[folderCacheKey]; + if (!cache) { + return undefined; + } + + const cached = this.getResultsFromCache(cache, config.filePattern, token); + if (cached) { + return cached.then(complete => { + const sortSW = StopWatch.create(); + return this.sortResults(config, complete.results, cache.scorerCache, token) + .then(sortedResults => { + if (token && token.isCancellationRequested) { + throw canceled(); + } + + return >{ + limitHit: complete.limitHit || typeof config.maxResults === 'number' && complete.results.length > config.maxResults, + results: sortedResults, + stats: { + fromCache: true, + detailStats: complete.stats, + type: 'fileIndexProvider', + resultCount: sortedResults.length, + sortingTime: sortSW.elapsed() + } + }; + }); + }); + } + return undefined; + } + + private sortResults(config: IRawSearchQuery, results: IInternalFileMatch[], scorerCache: ScorerCache, token: CancellationToken): TPromise { + // we use the same compare function that is used later when showing the results using fuzzy scoring + // this is very important because we are also limiting the number of results by config.maxResults + // and as such we want the top items to be included in this result set if the number of items + // exceeds config.maxResults. + const query = prepareQuery(config.filePattern); + const compare = (matchA: IInternalFileMatch, matchB: IInternalFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); + + return arrays.topAsync(results, compare, config.maxResults, 10000, token); + } + + private sendAsBatches(rawMatches: IInternalFileMatch[], onBatch: (batch: IFileMatch[]) => void, batchSize: number) { + const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); + if (batchSize && batchSize > 0) { + for (let i = 0; i < serializedMatches.length; i += batchSize) { + onBatch(serializedMatches.slice(i, i + batchSize)); + } + } else { + onBatch(serializedMatches); + } + } + + private getResultsFromCache(cache: Cache, searchValue: string, token: CancellationToken): TPromise> { + const cacheLookupSW = StopWatch.create(); + + if (path.isAbsolute(searchValue)) { + return null; // bypass cache if user looks up an absolute path where matching goes directly on disk + } + + // Find cache entries by prefix of search value + const hasPathSep = searchValue.indexOf(path.sep) >= 0; + let cacheRow: ICacheRow; + for (let previousSearch in cache.resultsToSearchCache) { + + // If we narrow down, we might be able to reuse the cached results + if (strings.startsWith(searchValue, previousSearch)) { + if (hasPathSep && previousSearch.indexOf(path.sep) < 0) { + continue; // since a path character widens the search for potential more matches, require it in previous search too + } + + const row = cache.resultsToSearchCache[previousSearch]; + cacheRow = { + promise: this.preventCancellation(row.promise), + resolved: row.resolved + }; + break; + } + } + + if (!cacheRow) { + return null; + } + + const cacheLookupTime = cacheLookupSW.elapsed(); + const cacheFilterSW = StopWatch.create(); + + return new TPromise>((c, e) => { + token.onCancellationRequested(() => e(canceled())); + + cacheRow.promise.then(complete => { + if (token && token.isCancellationRequested) { + e(canceled()); + } + + // Pattern match on results + let results: IInternalFileMatch[] = []; + const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); + for (let i = 0; i < complete.results.length; i++) { + let entry = complete.results[i]; + + // Check if this entry is a match for the search value + if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { + continue; + } + + results.push(entry); + } + + c(>{ + limitHit: complete.limitHit, + results, + stats: { + cacheWasResolved: cacheRow.resolved, + cacheLookupTime, + cacheFilterTime: cacheFilterSW.elapsed(), + cacheEntryCount: complete.results.length + } + }); + }, e); + }); + } + + private doSearch(engine: FileIndexSearchEngine, token: CancellationToken): TPromise> { + token.onCancellationRequested(() => engine.cancel()); + const results: IInternalFileMatch[] = []; + const onResult = match => results.push(match); + + return engine.search(onResult).then(result => { + return >{ + limitHit: result.isLimitHit, + results, + stats: result.stats + }; + }); + } + + public clearCache(cacheKey: string): TPromise { + if (!this.folderCacheKeys.has(cacheKey)) { + return TPromise.wrap(undefined); + } + + const expandedKeys = this.folderCacheKeys.get(cacheKey); + expandedKeys.forEach(key => delete this.caches[key]); + + this.folderCacheKeys.delete(cacheKey); + + return TPromise.as(undefined); + } + + private preventCancellation(promise: CancelablePromise): CancelablePromise { + return new class implements CancelablePromise { + cancel() { + // Do nothing + } + then(resolve, reject) { + return promise.then(resolve, reject); + } + catch(reject?) { + return this.then(undefined, reject); + } + }; + } +} + +interface ICacheRow { + promise: CancelablePromise>; + resolved: boolean; +} + +class Cache { + + public resultsToSearchCache: { [searchValue: string]: ICacheRow; } = Object.create(null); + + public scorerCache: ScorerCache = Object.create(null); +} + +const FileMatchItemAccessor = new class implements IItemAccessor { + + public getItemLabel(match: IInternalFileMatch): string { + return match.basename; // e.g. myFile.txt + } + + public getItemDescription(match: IInternalFileMatch): string { + return match.relativePath.substr(0, match.relativePath.length - match.basename.length - 1); // e.g. some/path/to/file + } + + public getItemPath(match: IInternalFileMatch): string { + return match.relativePath; // e.g. some/path/to/file/myFile.txt + } +}; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index c4cf892bd17..d84497b032b 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -4,23 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as pfs from 'vs/base/node/pfs'; -import * as extfs from 'vs/base/node/extfs'; import * as path from 'path'; -import * as arrays from 'vs/base/common/arrays'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as glob from 'vs/base/common/glob'; -import * as strings from 'vs/base/common/strings'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { IItemAccessor, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; -import { ICachedSearchStats, IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchQuery, ISearchCompleteStats, IRawFileMatch2 } from 'vs/platform/search/common/search'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import * as resources from 'vs/base/common/resources'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as extfs from 'vs/base/node/extfs'; +import { IFileMatch, IFileSearchProviderStats, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, ITextSearchResult } from 'vs/platform/search/common/search'; +import { FileIndexSearchManager, IDirectoryEntry, IDirectoryTree, IInternalFileMatch, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/api/node/extHostSearch.fileIndex'; import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { joinPath } from 'vs/base/common/resources'; - -type OneOrMore = T | T[]; export interface ISchemeTransformer { transformOutgoing(scheme: string): string; @@ -29,16 +26,18 @@ export interface ISchemeTransformer { export class ExtHostSearch implements ExtHostSearchShape { private readonly _proxy: MainThreadSearchShape; - private readonly _searchProvider = new Map(); + private readonly _fileSearchProvider = new Map(); + private readonly _textSearchProvider = new Map(); + private readonly _fileIndexProvider = new Map(); private _handlePool: number = 0; private _fileSearchManager: FileSearchManager; + private _fileIndexSearchManager: FileIndexSearchManager; - constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs, private _pfs = pfs) { + constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs) { this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); - this._fileSearchManager = new FileSearchManager( - (eventName: string, data: any) => this._proxy.$handleTelemetry(eventName, data), - this._pfs); + this._fileSearchManager = new FileSearchManager(); + this._fileIndexSearchManager = new FileIndexSearchManager(); } private _transformScheme(scheme: string): string { @@ -48,72 +47,69 @@ export class ExtHostSearch implements ExtHostSearchShape { return scheme; } - registerSearchProvider(scheme: string, provider: vscode.SearchProvider) { + registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider) { const handle = this._handlePool++; - this._searchProvider.set(handle, provider); - this._proxy.$registerSearchProvider(handle, this._transformScheme(scheme)); - return { - dispose: () => { - this._searchProvider.delete(handle); - this._proxy.$unregisterProvider(handle); - } - }; + this._fileSearchProvider.set(handle, provider); + this._proxy.$registerFileSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._fileSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); } - $provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery): TPromise { - const provider = this._searchProvider.get(handle); - if (!provider.provideFileSearchResults) { - return TPromise.as(undefined); - } + registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider) { + const handle = this._handlePool++; + this._textSearchProvider.set(handle, provider); + this._proxy.$registerTextSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._textSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + registerFileIndexProvider(scheme: string, provider: vscode.FileIndexProvider) { + const handle = this._handlePool++; + this._fileIndexProvider.set(handle, provider); + this._proxy.$registerFileIndexProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._fileSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); // TODO@roblou - unregisterFileIndexProvider + }); + } + + $provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery, token: CancellationToken): Thenable { + const provider = this._fileSearchProvider.get(handle); const query = reviveQuery(rawQuery); - return this._fileSearchManager.fileSearch(query, provider).then( - null, - null, - progress => { - if (Array.isArray(progress)) { - progress.forEach(p => { - this._proxy.$handleFindMatch(handle, session, p.resource); - }); - } else { - this._proxy.$handleFindMatch(handle, session, progress.resource); - } - }); + if (provider) { + return this._fileSearchManager.fileSearch(query, provider, batch => { + this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); + }, token); + } else { + const indexProvider = this._fileIndexProvider.get(handle); + return this._fileIndexSearchManager.fileSearch(query, indexProvider, batch => { + this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); + }, token); + } } - $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, rawQuery: IRawSearchQuery): TPromise { - const provider = this._searchProvider.get(handle); + $clearCache(cacheKey: string): Thenable { + // Actually called once per provider. + // Only relevant to file index search. + return this._fileIndexSearchManager.clearCache(cacheKey); + } + + $provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, rawQuery: IRawSearchQuery, token: CancellationToken): Thenable { + const provider = this._textSearchProvider.get(handle); if (!provider.provideTextSearchResults) { return TPromise.as(undefined); } const query = reviveQuery(rawQuery); const engine = new TextSearchEngine(pattern, query, provider, this._extfs); - return engine.search().then( - null, - null, - progress => { - this._proxy.$handleFindMatch(handle, session, progress); - }); + return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); } } -/** - * Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns - */ -function resolvePatternsForProvider(globalPattern: glob.IExpression, folderPattern: glob.IExpression): string[] { - const merged = { - ...(globalPattern || {}), - ...(folderPattern || {}) - }; - - return Object.keys(merged) - .filter(key => { - const value = merged[key]; - return typeof value === 'boolean' && value; - }); -} - function reviveQuery(rawQuery: IRawSearchQuery): ISearchQuery { return { ...rawQuery, @@ -132,45 +128,38 @@ function reviveFolderQuery(rawFolderQuery: IFolderQuery): IFolder } class TextSearchResultsCollector { - private _batchedCollector: BatchedCollector; + private _batchedCollector: BatchedCollector; private _currentFolderIdx: number; - private _currentRelativePath: string; - private _currentFileMatch: IRawFileMatch2; + private _currentUri: URI; + private _currentFileMatch: IFileMatch; - constructor(private folderQueries: IFolderQuery[], private _onResult: (result: IRawFileMatch2[]) => void) { - this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); + constructor(private _onResult: (result: IFileMatch[]) => void) { + this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); } add(data: vscode.TextSearchResult, folderIdx: number): void { // Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector. // This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search // providers that send results in random order. We could do this step afterwards instead. - if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || this._currentRelativePath !== data.path)) { + if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || resources.isEqual(this._currentUri, data.uri))) { this.pushToCollector(); this._currentFileMatch = null; } if (!this._currentFileMatch) { - const resource = joinPath(this.folderQueries[folderIdx].folder, data.path); this._currentFileMatch = { - resource, - lineMatches: [] + resource: data.uri, + matches: [] }; } - // TODO@roblou - line text is sent for every match - const matchRange = data.preview.match; - this._currentFileMatch.lineMatches.push({ - lineNumber: data.range.start.line, - preview: data.preview.text, - offsetAndLengths: [[matchRange.start.character, matchRange.end.character - matchRange.start.character]] - }); + this._currentFileMatch.matches.push(extensionResultToFrontendResult(data)); } private pushToCollector(): void { const size = this._currentFileMatch ? - this._currentFileMatch.lineMatches.reduce((acc, match) => acc + match.offsetAndLengths.length, 0) : + this._currentFileMatch.matches.length : 0; this._batchedCollector.addItem(this._currentFileMatch, size); } @@ -180,11 +169,31 @@ class TextSearchResultsCollector { this._batchedCollector.flush(); } - private sendItems(items: IRawFileMatch2 | IRawFileMatch2[]): void { - this._onResult(Array.isArray(items) ? items : [items]); + private sendItems(items: IFileMatch[]): void { + this._onResult(items); } } +function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult { + return { + preview: { + match: { + startLineNumber: data.preview.match.start.line, + startColumn: data.preview.match.start.character, + endLineNumber: data.preview.match.end.line, + endColumn: data.preview.match.end.character + }, + text: data.preview.text + }, + range: { + startLineNumber: data.range.start.line, + startColumn: data.range.start.character, + endLineNumber: data.range.end.line, + endColumn: data.range.end.character + } + }; +} + /** * Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every * set of items collected. @@ -202,7 +211,7 @@ class BatchedCollector { private batchSize = 0; private timeoutHandle: number; - constructor(private maxBatchSize: number, private cb: (items: T | T[]) => void) { + constructor(private maxBatchSize: number, private cb: (items: T[]) => void) { } addItem(item: T, size: number): void { @@ -210,11 +219,7 @@ class BatchedCollector { return; } - if (this.maxBatchSize > 0) { - this.addItemToBatch(item, size); - } else { - this.cb(item); - } + this.addItemToBatch(item, size); } addItems(items: T[], size: number): void { @@ -271,139 +276,34 @@ class BatchedCollector { } } -interface IDirectoryEntry { - base: URI; - relativePath: string; - basename: string; -} - -interface IDirectoryTree { - rootEntries: IDirectoryEntry[]; - pathToEntries: { [relativePath: string]: IDirectoryEntry[] }; -} - -interface IInternalFileMatch { - base: URI; - relativePath?: string; // Not present for extraFiles or absolute path matches - basename: string; - size?: number; -} - -class QueryGlobTester { - - private _excludeExpression: glob.IExpression; - private _parsedExcludeExpression: glob.ParsedExpression; - - private _parsedIncludeExpression: glob.ParsedExpression; - - constructor(config: ISearchQuery, folderQuery: IFolderQuery) { - this._excludeExpression = { - ...(config.excludePattern || {}), - ...(folderQuery.excludePattern || {}) - }; - this._parsedExcludeExpression = glob.parse(this._excludeExpression); - - // Empty includeExpression means include nothing, so no {} shortcuts - let includeExpression: glob.IExpression = config.includePattern; - if (folderQuery.includePattern) { - if (includeExpression) { - includeExpression = { - ...includeExpression, - ...folderQuery.includePattern - }; - } else { - includeExpression = folderQuery.includePattern; - } - } - - if (includeExpression) { - this._parsedIncludeExpression = glob.parse(includeExpression); - } - } - - /** - * Guaranteed sync - siblingsFn should not return a promise. - */ - public includedInQuerySync(testPath: string, basename?: string, siblingsFn?: () => string[]): boolean { - if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, siblingsFn)) { - return false; - } - - if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, siblingsFn)) { - return false; - } - - return true; - } - - /** - * Guaranteed async. - */ - public includedInQuery(testPath: string, basename?: string, siblingsFn?: () => string[] | TPromise): TPromise { - const excludeP = this._parsedExcludeExpression ? - TPromise.as(this._parsedExcludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : - TPromise.wrap(false); - - return excludeP.then(excluded => { - if (excluded) { - return false; - } - - return this._parsedIncludeExpression ? - TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : - TPromise.wrap(true); - }).then(included => { - return included; - }); - } - - public hasSiblingExcludeClauses(): boolean { - return hasSiblingClauses(this._excludeExpression); - } -} - -function hasSiblingClauses(pattern: glob.IExpression): boolean { - for (let key in pattern) { - if (typeof pattern[key] !== 'boolean') { - return true; - } - } - - return false; -} - class TextSearchEngine { - private activeCancellationTokens = new Set(); private collector: TextSearchResultsCollector; private isLimitHit: boolean; private resultCount = 0; - private isCanceled: boolean; - constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.SearchProvider, private _extfs: typeof extfs) { + constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs) { } - public cancel(): void { - this.isCanceled = true; - this.activeCancellationTokens.forEach(t => t.cancel()); - this.activeCancellationTokens = new Set(); - } - - public search(): PPromise<{ limitHit: boolean }, IRawFileMatch2[]> { + public search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): TPromise { const folderQueries = this.config.folderQueries; + const tokenSource = new CancellationTokenSource(); + token.onCancellationRequested(() => tokenSource.cancel()); - return new PPromise<{ limitHit: boolean }, IRawFileMatch2[]>((resolve, reject, _onResult) => { - this.collector = new TextSearchResultsCollector(this.config.folderQueries, _onResult); + return new TPromise((resolve, reject) => { + this.collector = new TextSearchResultsCollector(onProgress); + let isCanceled = false; const onResult = (match: vscode.TextSearchResult, folderIdx: number) => { - if (this.isCanceled) { + if (isCanceled) { return; } if (this.resultCount >= this.config.maxResults) { this.isLimitHit = true; - this.cancel(); + isCanceled = true; + tokenSource.cancel(); } if (!this.isLimitHit) { @@ -413,12 +313,19 @@ class TextSearchEngine { }; // For each root folder - PPromise.join(folderQueries.map((fq, i) => { - return this.searchInFolder(fq).then(null, null, r => onResult(r, i)); + TPromise.join(folderQueries.map((fq, i) => { + return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token); })).then(() => { + tokenSource.dispose(); this.collector.flush(); - resolve({ limitHit: this.isLimitHit }); + resolve({ + limitHit: this.isLimitHit, + stats: { + type: 'textSearchProvider' + } + }); }, (errs: Error[]) => { + tokenSource.dispose(); const errMsg = errs .map(err => toErrorMessage(err)) .filter(msg => !!msg)[0]; @@ -428,20 +335,20 @@ class TextSearchEngine { }); } - private searchInFolder(folderQuery: IFolderQuery): PPromise { - let cancellation = new CancellationTokenSource(); - return new PPromise((resolve, reject, onResult) => { + private searchInFolder(folderQuery: IFolderQuery, onResult: (result: vscode.TextSearchResult) => void, token: CancellationToken): TPromise { + return new TPromise((resolve, reject) => { const queryTester = new QueryGlobTester(this.config, folderQuery); const testingPs = []; const progress = { report: (result: vscode.TextSearchResult) => { - const siblingFn = folderQuery.folder.scheme === 'file' && (() => { - return this.readdir(path.dirname(path.join(folderQuery.folder.fsPath, result.path))); + const hasSibling = folderQuery.folder.scheme === 'file' && glob.hasSiblingPromiseFn(() => { + return this.readdir(path.dirname(result.uri.fsPath)); }); + const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath); testingPs.push( - queryTester.includedInQuery(result.path, path.basename(result.path), siblingFn) + queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling) .then(included => { if (included) { onResult(result); @@ -453,22 +360,14 @@ class TextSearchEngine { const searchOptions = this.getSearchOptionsForFolder(folderQuery); new TPromise(resolve => process.nextTick(resolve)) .then(() => { - this.activeCancellationTokens.add(cancellation); - return this.provider.provideTextSearchResults(patternInfoToQuery(this.pattern), searchOptions, progress, cancellation.token); + return this.provider.provideTextSearchResults(patternInfoToQuery(this.pattern), searchOptions, progress, token); }) .then(() => { - this.activeCancellationTokens.delete(cancellation); return TPromise.join(testingPs); }) .then( - () => { - cancellation.dispose(); - resolve(null); - }, - err => { - cancellation.dispose(); - reject(err); - }); + () => resolve(null), + reject); }); } @@ -495,7 +394,9 @@ class TextSearchEngine { useIgnoreFiles: !this.config.disregardIgnoreFiles, followSymlinks: !this.config.ignoreSymlinks, encoding: this.config.fileEncoding, - maxFileSize: this.config.maxFileSize + maxFileSize: this.config.maxFileSize, + maxResults: this.config.maxResults, + previewOptions: this.config.previewOptions }; } } @@ -511,39 +412,26 @@ function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery { class FileSearchEngine { private filePattern: string; - private normalizedFilePatternLowercase: string; private includePattern: glob.ParsedExpression; private maxResults: number; private exists: boolean; - // private maxFilesize: number; private isLimitHit: boolean; private resultCount: number; private isCanceled: boolean; private activeCancellationTokens: Set; - // private filesWalked: number; - // private directoriesWalked: number; - private globalExcludePattern: glob.ParsedExpression; - constructor(private config: ISearchQuery, private provider: vscode.SearchProvider, private _pfs: typeof pfs) { + constructor(private config: ISearchQuery, private provider: vscode.FileSearchProvider) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; this.exists = config.exists; - // this.maxFilesize = config.maxFileSize || null; this.resultCount = 0; this.isLimitHit = false; this.activeCancellationTokens = new Set(); - // this.filesWalked = 0; - // this.directoriesWalked = 0; - - if (this.filePattern) { - this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); - } - this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern); } @@ -553,128 +441,113 @@ class FileSearchEngine { this.activeCancellationTokens = new Set(); } - public search(): PPromise<{ isLimitHit: boolean }, IInternalFileMatch> { + public search(_onResult: (match: IInternalFileMatch) => void): TPromise { const folderQueries = this.config.folderQueries; - return new PPromise<{ isLimitHit: boolean }, IInternalFileMatch>((resolve, reject, _onResult) => { + return new TPromise((resolve, reject) => { const onResult = (match: IInternalFileMatch) => { this.resultCount++; _onResult(match); }; // Support that the file pattern is a full path to a file that exists - this.checkFilePatternAbsoluteMatch().then(({ exists, size }) => { - if (this.isCanceled) { - return resolve({ isLimitHit: this.isLimitHit }); - } + if (this.isCanceled) { + return resolve({ limitHit: this.isLimitHit }); + } - // Report result from file pattern if matching - if (exists) { - onResult({ - base: URI.file(this.filePattern), - basename: path.basename(this.filePattern), - size + // For each extra file + if (this.config.extraFileResources) { + this.config.extraFileResources + .forEach(extraFile => { + const extraFileStr = extraFile.toString(); // ? + const basename = path.basename(extraFileStr); + if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { + return; // excluded + } + + // File: Check for match on file pattern and include pattern + this.matchFile(onResult, { base: extraFile, basename }); }); + } - // Optimization: a match on an absolute path is a good result and we do not - // continue walking the entire root paths array for other matches because - // it is very unlikely that another file would match on the full absolute path - return resolve({ isLimitHit: this.isLimitHit }); - } - - // For each extra file - if (this.config.extraFileResources) { - this.config.extraFileResources - .forEach(extraFile => { - const extraFileStr = extraFile.toString(); // ? - const basename = path.basename(extraFileStr); - if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { - return; // excluded - } - - // File: Check for match on file pattern and include pattern - this.matchFile(onResult, { base: extraFile, basename }); - }); - } - - // For each root folder - PPromise.join(folderQueries.map(fq => { - return this.searchInFolder(fq).then(null, null, onResult); - })).then(() => { - resolve({ isLimitHit: this.isLimitHit }); - }, (errs: Error[]) => { - const errMsg = errs - .map(err => toErrorMessage(err)) - .filter(msg => !!msg)[0]; - - reject(new Error(errMsg)); + // For each root folder + TPromise.join(folderQueries.map(fq => { + return this.searchInFolder(fq, onResult); + })).then(stats => { + resolve({ + limitHit: this.isLimitHit, + stats: stats[0] // Only looking at single-folder workspace stats... }); + }, (errs: Error[]) => { + const errMsg = errs + .map(err => toErrorMessage(err)) + .filter(msg => !!msg)[0]; + + reject(new Error(errMsg)); }); }); } - private searchInFolder(fq: IFolderQuery): PPromise { + private searchInFolder(fq: IFolderQuery, onResult: (match: IInternalFileMatch) => void): TPromise { let cancellation = new CancellationTokenSource(); - return new PPromise((resolve, reject, onResult) => { + return new TPromise((resolve, reject) => { const options = this.getSearchOptionsForFolder(fq); - let filePatternSeen = false; const tree = this.initDirectoryTree(); const queryTester = new QueryGlobTester(this.config, fq); const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses(); - const onProviderResult = (relativePath: string) => { - if (this.isCanceled) { - return; - } - - if (noSiblingsClauses) { - if (relativePath === this.filePattern) { - filePatternSeen = true; - } - - const basename = path.basename(relativePath); - this.matchFile(onResult, { base: fq.folder, relativePath, basename }); - - return; - } - - // TODO: Optimize siblings clauses with ripgrep here. - this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); - }; - - new TPromise(resolve => process.nextTick(resolve)) + let providerSW: StopWatch; + new TPromise(_resolve => process.nextTick(_resolve)) .then(() => { this.activeCancellationTokens.add(cancellation); - return this.provider.provideFileSearchResults(options, { report: onProviderResult }, cancellation.token); + + providerSW = StopWatch.create(); + return this.provider.provideFileSearchResults( + { + pattern: this.config.filePattern || '' + }, + options, + cancellation.token); }) - .then(() => { + .then(results => { + const providerTime = providerSW.elapsed(); + const postProcessSW = StopWatch.create(); + + if (this.isCanceled) { + return null; + } + + if (results) { + results.forEach(result => { + const relativePath = path.relative(fq.folder.fsPath, result.fsPath); + + if (noSiblingsClauses) { + const basename = path.basename(result.fsPath); + this.matchFile(onResult, { base: fq.folder, relativePath, basename }); + + return; + } + + // TODO: Optimize siblings clauses with ripgrep here. + this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); + }); + } + this.activeCancellationTokens.delete(cancellation); if (this.isCanceled) { return null; } - if (noSiblingsClauses && this.isLimitHit) { - if (!filePatternSeen) { - // If the limit was hit, check whether filePattern is an exact relative match because it must be included - return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => { - if (exists) { - onResult({ - base: fq.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - }); - } - } - this.matchDirectoryTree(tree, queryTester, onResult); - return null; + return { + providerTime, + postProcessTime: postProcessSW.elapsed() + }; }).then( - () => { + stats => { cancellation.dispose(); - resolve(undefined); + resolve(stats); }, err => { cancellation.dispose(); @@ -692,7 +565,8 @@ class FileSearchEngine { excludes, includes, useIgnoreFiles: !this.config.disregardIgnoreFiles, - followSymlinks: !this.config.ignoreSymlinks + followSymlinks: !this.config.ignoreSymlinks, + maxResults: this.config.maxResults }; } @@ -734,7 +608,7 @@ class FileSearchEngine { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { - // self.directoriesWalked++; + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -743,7 +617,7 @@ class FileSearchEngine { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (!queryTester.includedInQuerySync(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -751,7 +625,6 @@ class FileSearchEngine { if (sub) { matchDirectory(sub); } else { - // self.filesWalked++; if (relativePath === filePattern) { continue; // ignore file if its path matches with the file pattern because that is already matched above } @@ -767,64 +640,8 @@ class FileSearchEngine { matchDirectory(rootEntries); } - public getStats(): any { - return null; - // return { - // fromCache: false, - // traversal: Traversal[this.traversal], - // errors: this.errors, - // fileWalkStartTime: this.fileWalkStartTime, - // fileWalkResultTime: Date.now(), - // directoriesWalked: this.directoriesWalked, - // filesWalked: this.filesWalked, - // resultCount: this.resultCount, - // cmdForkResultTime: this.cmdForkResultTime, - // cmdResultCount: this.cmdResultCount - // }; - } - - /** - * Return whether the file pattern is an absolute path to a file that exists. - * TODO@roblou should use FS provider? - */ - private checkFilePatternAbsoluteMatch(): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || !path.isAbsolute(this.filePattern)) { - return TPromise.wrap({ exists: false }); - } - - return this._pfs.stat(this.filePattern) - .then(stat => { - return { - exists: !stat.isDirectory(), - size: stat.size - }; - }, err => { - return { - exists: false - }; - }); - } - - private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') { - return TPromise.wrap({ exists: false }); - } - - const absolutePath = path.join(base.fsPath, this.filePattern); - return this._pfs.stat(absolutePath).then(stat => { - return { - exists: !stat.isDirectory(), - size: stat.size - }; - }, err => { - return { - exists: false - }; - }); - } - private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void { - if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) { + if (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename)) { if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) { this.isLimitHit = true; this.cancel(); @@ -835,344 +652,81 @@ class FileSearchEngine { } } } +} - private isFilePatternMatch(path: string): boolean { - // Check for search pattern - if (this.filePattern) { - if (this.filePattern === '*') { - return true; // support the all-matching wildcard - } - - return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); - } - - // No patterns means we match all - return true; - } +interface IInternalSearchComplete { + limitHit: boolean; + stats?: IFileSearchProviderStats; } class FileSearchManager { private static readonly BATCH_SIZE = 512; - private caches: { [cacheKey: string]: Cache; } = Object.create(null); + fileSearch(config: ISearchQuery, provider: vscode.FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): TPromise { + const engine = new FileSearchEngine(config, provider); - constructor(private telemetryCallback: (eventName: string, data: any) => void, private _pfs: typeof pfs) { } + let resultCount = 0; + const onInternalResult = (batch: IInternalFileMatch[]) => { + resultCount += batch.length; + onBatch(batch.map(m => this.rawMatchToSearchItem(m))); + }; - public fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise> { - if (config.sortByScore) { - let sortedSearch = this.trySortedSearchFromCache(config); - if (!sortedSearch) { - const engineConfig = config.maxResults ? - { - ...config, - ...{ maxResults: null } - } : - config; - - const engine = new FileSearchEngine(engineConfig, provider, this._pfs); - sortedSearch = this.doSortedSearch(engine, provider, config); - } - - return new PPromise>((c, e, p) => { - process.nextTick(() => { // allow caller to register progress callback first - sortedSearch.then(([result, rawMatches]) => { - const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); - this.sendProgress(serializedMatches, p, FileSearchManager.BATCH_SIZE); - c(result); - }, e, p); - }); - }, () => { - sortedSearch.cancel(); - }); - } - - let searchPromise: PPromise>; - return new PPromise>((c, e, p) => { - const engine = new FileSearchEngine(config, provider, this._pfs); - searchPromise = this.doSearch(engine, provider, FileSearchManager.BATCH_SIZE) - .then(c, e, progress => { - if (Array.isArray(progress)) { - p(progress.map(m => this.rawMatchToSearchItem(m))); - } else if ((progress).relativePath) { - p(this.rawMatchToSearchItem(progress)); + return this.doSearch(engine, FileSearchManager.BATCH_SIZE, onInternalResult, token).then( + result => { + return { + limitHit: result.limitHit, + stats: { + fromCache: false, + type: 'fileSearchProvider', + resultCount, + detailStats: result.stats } - }); - }, () => { - searchPromise.cancel(); - }); + }; + }); } private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch { - return { - resource: joinPath(match.base, match.relativePath) - }; - } - - private doSortedSearch(engine: FileSearchEngine, provider: vscode.SearchProvider, config: IRawSearchQuery): PPromise<[ISearchCompleteStats, IInternalFileMatch[]]> { - let searchPromise: PPromise>; - let allResultsPromise = new PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>((c, e, p) => { - let results: IInternalFileMatch[] = []; - searchPromise = this.doSearch(engine, provider, -1) - .then(result => { - c([result, results]); - this.telemetryCallback('fileSearch', null); - }, e, progress => { - if (Array.isArray(progress)) { - results = progress; - } else { - p(progress); - } - }); - }, () => { - searchPromise.cancel(); - }); - - let cache: Cache; - if (config.cacheKey) { - cache = this.getOrCreateCache(config.cacheKey); - cache.resultsToSearchCache[config.filePattern] = allResultsPromise; - allResultsPromise.then(null, err => { - delete cache.resultsToSearchCache[config.filePattern]; - }); - allResultsPromise = this.preventCancellation(allResultsPromise); - } - - let chained: TPromise; - return new PPromise<[ISearchCompleteStats, IInternalFileMatch[]]>((c, e, p) => { - chained = allResultsPromise.then(([result, results]) => { - const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); - const unsortedResultTime = Date.now(); - return this.sortResults(config, results, scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); - - c([{ - stats: { - ...result.stats, - ...{ unsortedResultTime, sortedResultTime } - }, - limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults - }, sortedResults]); - }); - }, e, p); - }, () => { - chained.cancel(); - }); - } - - private getOrCreateCache(cacheKey: string): Cache { - const existing = this.caches[cacheKey]; - if (existing) { - return existing; - } - return this.caches[cacheKey] = new Cache(); - } - - private trySortedSearchFromCache(config: IRawSearchQuery): TPromise<[ISearchCompleteStats, IInternalFileMatch[]]> { - const cache = config.cacheKey && this.caches[config.cacheKey]; - if (!cache) { - return undefined; - } - - const cacheLookupStartTime = Date.now(); - const cached = this.getResultsFromCache(cache, config.filePattern); - if (cached) { - let chained: TPromise; - return new TPromise<[ISearchCompleteStats, IInternalFileMatch[]]>((c, e) => { - chained = cached.then(([result, results, cacheStats]) => { - const cacheLookupResultTime = Date.now(); - return this.sortResults(config, results, cache.scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); - - const stats: ICachedSearchStats = { - fromCache: true, - cacheLookupStartTime: cacheLookupStartTime, - cacheFilterStartTime: cacheStats.cacheFilterStartTime, - cacheLookupResultTime: cacheLookupResultTime, - cacheEntryCount: cacheStats.cacheFilterResultCount, - resultCount: results.length - }; - if (config.sortByScore) { - stats.unsortedResultTime = cacheLookupResultTime; - stats.sortedResultTime = sortedResultTime; - } - if (!cacheStats.cacheWasResolved) { - stats.joined = result.stats; - } - c([ - { - limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults, - stats: stats - }, - sortedResults - ]); - }); - }, e); - }, () => { - chained.cancel(); - }); - } - return undefined; - } - - private sortResults(config: IRawSearchQuery, results: IInternalFileMatch[], scorerCache: ScorerCache): TPromise { - // we use the same compare function that is used later when showing the results using fuzzy scoring - // this is very important because we are also limiting the number of results by config.maxResults - // and as such we want the top items to be included in this result set if the number of items - // exceeds config.maxResults. - const query = prepareQuery(config.filePattern); - const compare = (matchA: IInternalFileMatch, matchB: IInternalFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); - - return arrays.topAsync(results, compare, config.maxResults, 10000); - } - - private sendProgress(results: IFileMatch[], progressCb: (batch: IFileMatch[]) => void, batchSize: number) { - if (batchSize && batchSize > 0) { - for (let i = 0; i < results.length; i += batchSize) { - progressCb(results.slice(i, i + batchSize)); - } + if (match.relativePath) { + return { + resource: resources.joinPath(match.base, match.relativePath) + }; } else { - progressCb(results); + // extraFileResources + return { + resource: match.base + }; } } - private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISearchCompleteStats, IInternalFileMatch[], CacheStats]> { - if (path.isAbsolute(searchValue)) { - return null; // bypass cache if user looks up an absolute path where matching goes directly on disk - } - - // Find cache entries by prefix of search value - const hasPathSep = searchValue.indexOf(path.sep) >= 0; - let cached: PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>; - let wasResolved: boolean; - for (let previousSearch in cache.resultsToSearchCache) { - - // If we narrow down, we might be able to reuse the cached results - if (strings.startsWith(searchValue, previousSearch)) { - if (hasPathSep && previousSearch.indexOf(path.sep) < 0) { - continue; // since a path character widens the search for potential more matches, require it in previous search too - } - - const c = cache.resultsToSearchCache[previousSearch]; - c.then(() => { wasResolved = false; }); - wasResolved = true; - cached = this.preventCancellation(c); - break; - } - } - - if (!cached) { - return null; - } - - return new PPromise<[ISearchCompleteStats, IInternalFileMatch[], CacheStats]>((c, e, p) => { - cached.then(([complete, cachedEntries]) => { - const cacheFilterStartTime = Date.now(); - - // Pattern match on results - let results: IInternalFileMatch[] = []; - const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); - for (let i = 0; i < cachedEntries.length; i++) { - let entry = cachedEntries[i]; - - // Check if this entry is a match for the search value - if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { - continue; - } - - results.push(entry); - } - - c([complete, results, { - cacheWasResolved: wasResolved, - cacheFilterStartTime: cacheFilterStartTime, - cacheFilterResultCount: cachedEntries.length - }]); - }, e, p); - }, () => { - cached.cancel(); - }); - } - - private doSearch(engine: FileSearchEngine, provider: vscode.SearchProvider, batchSize?: number): PPromise> { - return new PPromise>((c, e, p) => { - let batch: IInternalFileMatch[] = []; - engine.search().then(result => { - if (batch.length) { - p(batch); - } - - c({ - limitHit: result.isLimitHit, - stats: engine.getStats() // TODO@roblou - }); - }, error => { - if (batch.length) { - p(batch); - } - - e(error); - }, match => { - if (match) { - if (batchSize) { - batch.push(match); - if (batchSize > 0 && batch.length >= batchSize) { - p(batch); - batch = []; - } - } else { - p(match); - } - } - }); - }, () => { + private doSearch(engine: FileSearchEngine, batchSize: number, onResultBatch: (matches: IInternalFileMatch[]) => void, token: CancellationToken): TPromise { + token.onCancellationRequested(() => { engine.cancel(); }); - } - public clearCache(cacheKey: string): TPromise { - delete this.caches[cacheKey]; - return TPromise.as(undefined); - } + const _onResult = match => { + if (match) { + batch.push(match); + if (batchSize > 0 && batch.length >= batchSize) { + onResultBatch(batch); + batch = []; + } + } + }; - private preventCancellation(promise: PPromise): PPromise { - return new PPromise((c, e, p) => { - // Allow for piled up cancellations to come through first. - process.nextTick(() => { - promise.then(c, e, p); - }); - }, () => { - // Do not propagate. + let batch: IInternalFileMatch[] = []; + return engine.search(_onResult).then(result => { + if (batch.length) { + onResultBatch(batch); + } + + return result; + }, error => { + if (batch.length) { + onResultBatch(batch); + } + + return TPromise.wrapError(error); }); } } - -class Cache { - - public resultsToSearchCache: { [searchValue: string]: PPromise<[ISearchCompleteStats, IInternalFileMatch[]], OneOrMore>; } = Object.create(null); - - public scorerCache: ScorerCache = Object.create(null); -} - -const FileMatchItemAccessor = new class implements IItemAccessor { - - public getItemLabel(match: IInternalFileMatch): string { - return match.basename; // e.g. myFile.txt - } - - public getItemDescription(match: IInternalFileMatch): string { - return match.relativePath.substr(0, match.relativePath.length - match.basename.length - 1); // e.g. some/path/to/file - } - - public getItemPath(match: IInternalFileMatch): string { - return match.relativePath; // e.g. some/path/to/file/myFile.txt - } -}; - -interface CacheStats { - cacheWasResolved: boolean; - cacheFilterStartTime: number; - cacheFilterResultCount: number; -} diff --git a/src/vs/workbench/api/node/extHostStorage.ts b/src/vs/workbench/api/node/extHostStorage.ts index 5a36bd5d550..534d5ef0c14 100644 --- a/src/vs/workbench/api/node/extHostStorage.ts +++ b/src/vs/workbench/api/node/extHostStorage.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { MainContext, MainThreadStorageShape, IMainContext } from './extHost.protocol'; export class ExtHostStorage { @@ -15,11 +14,11 @@ export class ExtHostStorage { this._proxy = mainContext.getProxy(MainContext.MainThreadStorage); } - getValue(shared: boolean, key: string, defaultValue?: T): TPromise { + getValue(shared: boolean, key: string, defaultValue?: T): Thenable { return this._proxy.$getValue(shared, key).then(value => value || defaultValue); } - setValue(shared: boolean, key: string, value: any): TPromise { + setValue(shared: boolean, key: string, value: any): Thenable { return this._proxy.$setValue(shared, key, value); } } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 9f298b613df..f6f4d07fd70 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import * as Objects from 'vs/base/common/objects'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { asThenable } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @@ -27,6 +27,7 @@ import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDeb import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { CancellationToken } from 'vs/base/common/cancellation'; /* namespace ProblemPattern { @@ -803,7 +804,7 @@ export class ExtHostTask implements ExtHostTaskShape { return result; } - public terminateTask(execution: vscode.TaskExecution): TPromise { + public terminateTask(execution: vscode.TaskExecution): Thenable { if (!(execution instanceof TaskExecutionImpl)) { throw new Error('No valid task execution provided'); } @@ -860,22 +861,32 @@ export class ExtHostTask implements ExtHostTaskShape { } } - public $provideTasks(handle: number): TPromise { + public $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable { let handler = this._handlers.get(handle); if (!handler) { return TPromise.wrapError(new Error('no handler found')); } - return asWinJsPromise(token => handler.provider.provideTasks(token)).then(value => { + return asThenable(() => handler.provider.provideTasks(CancellationToken.None)).then(value => { + let sanitized: vscode.Task[] = []; + for (let task of value) { + if (task.definition && validTypes[task.definition.type] === true) { + sanitized.push(task); + } else { + sanitized.push(task); + console.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); + } + } let workspaceFolders = this._workspaceService.getWorkspaceFolders(); return { - tasks: Tasks.from(value, workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0] : undefined, handler.extension), + tasks: Tasks.from(sanitized, workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0] : undefined, handler.extension), extension: handler.extension }; }); } - public $resolveVariables(uri: URI, variables: string[]): any { - let result = Object.create(null); + public $resolveVariables(uriComponents: UriComponents, variables: string[]): any { + let uri: URI = URI.revive(uriComponents); + let result: { [key: string]: string; } = Object.create(null); let workspaceFolder = this._workspaceService.resolveWorkspaceFolder(uri); let resolver = new ExtHostVariableResolverService(this._workspaceService, this._editorService, this._configurationService); let ws: IWorkspaceFolder = { @@ -887,7 +898,7 @@ export class ExtHostTask implements ExtHostTaskShape { } }; for (let variable of variables) { - result.push(variable, resolver.resolve(ws, variable)); + result[variable] = resolver.resolve(ws, variable); } return result; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index c4f2e996461..e9bc45504fc 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,28 +5,77 @@ 'use strict'; import * as vscode from 'vscode'; -import * as cp from 'child_process'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; -import Uri from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; -import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; +import { EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; -export class ExtHostTerminal implements vscode.Terminal { - private _name: string; - private _id: number; - private _proxy: MainThreadTerminalServiceShape; - private _disposed: boolean; - private _queuedRequests: ApiRequest[]; +const RENDERER_NO_PROCESS_ID = -1; + +export class BaseExtHostTerminal { + public _id: number; + protected _idPromise: Promise; + private _idPromiseComplete: (value: number) => any; + private _disposed: boolean = false; + private _queuedRequests: ApiRequest[] = []; + + constructor( + protected _proxy: MainThreadTerminalServiceShape, + id?: number + ) { + this._idPromise = new Promise(c => { + if (id !== undefined) { + this._id = id; + c(id); + } else { + this._idPromiseComplete = c; + } + }); + } + + public dispose(): void { + if (!this._disposed) { + this._disposed = true; + this._queueApiRequest(this._proxy.$dispose, []); + } + } + + protected _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } + } + + protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void { + const request: ApiRequest = new ApiRequest(callback, args); + if (!this._id) { + this._queuedRequests.push(request); + return; + } + request.run(this._proxy, this._id); + } + + public _runQueuedRequests(id: number): void { + this._id = id; + this._idPromiseComplete(id); + this._queuedRequests.forEach((r) => { + r.run(this._proxy, this._id); + }); + this._queuedRequests.length = 0; + } +} + +export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _pidPromiseComplete: (value: number) => any; private readonly _onData: Emitter = new Emitter(); - public get onData(): Event { + public get onDidWriteData(): Event { // Tell the main side to start sending data if it's not already this._proxy.$registerOnDataListener(this._id); return this._onData && this._onData.event; @@ -34,17 +83,17 @@ export class ExtHostTerminal implements vscode.Terminal { constructor( proxy: MainThreadTerminalServiceShape, - name: string = '', - id?: number + private _name: string, + id?: number, + pid?: number ) { - this._proxy = proxy; - this._name = name; - if (id) { - this._id = id; - } - this._queuedRequests = []; + super(proxy, id); this._pidPromise = new Promise(c => { - this._pidPromiseComplete = c; + if (pid === RENDERER_NO_PROCESS_ID) { + c(undefined); + } else { + this._pidPromiseComplete = c; + } }); } @@ -56,11 +105,7 @@ export class ExtHostTerminal implements vscode.Terminal { waitOnExit?: boolean ): void { this._proxy.$createTerminal(this._name, shellPath, shellArgs, cwd, env, waitOnExit).then((id) => { - this._id = id; - this._queuedRequests.forEach((r) => { - r.run(this._proxy, this._id); - }); - this._queuedRequests = []; + this._runQueuedRequests(id); }); } @@ -87,13 +132,6 @@ export class ExtHostTerminal implements vscode.Terminal { this._queueApiRequest(this._proxy.$hide, []); } - public dispose(): void { - if (!this._disposed) { - this._disposed = true; - this._queueApiRequest(this._proxy.$dispose, []); - } - } - public _setProcessId(processId: number): void { // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { @@ -105,34 +143,99 @@ export class ExtHostTerminal implements vscode.Terminal { public _fireOnData(data: string): void { this._onData.fire(data); } +} - private _queueApiRequest(callback: (...args: any[]) => void, args: any[]) { - let request: ApiRequest = new ApiRequest(callback, args); - if (!this._id) { - this._queuedRequests.push(request); - return; - } - request.run(this._proxy, this._id); +export class ExtHostTerminalRenderer extends BaseExtHostTerminal implements vscode.TerminalRenderer { + public get name(): string { return this._name; } + public set name(newName: string) { + this._name = newName; + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererSetName, [this._name]); } - private _checkDisposed() { - if (this._disposed) { - throw new Error('Terminal has already been disposed'); + private readonly _onInput: Emitter = new Emitter(); + public get onDidAcceptInput(): Event { + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererRegisterOnInputListener, [this._id]); + // Tell the main side to start sending data if it's not already + // this._proxy.$terminalRendererRegisterOnDataListener(this._id); + return this._onInput && this._onInput.event; + } + + private _dimensions: vscode.TerminalDimensions | undefined; + public get dimensions(): vscode.TerminalDimensions { return this._dimensions; } + public set dimensions(dimensions: vscode.TerminalDimensions) { + this._checkDisposed(); + this._dimensions = dimensions; + this._queueApiRequest(this._proxy.$terminalRendererSetDimensions, [dimensions]); + } + + private _maximumDimensions: vscode.TerminalDimensions; + public get maximumDimensions(): vscode.TerminalDimensions { + if (!this._maximumDimensions) { + return undefined; } + return { + rows: this._maximumDimensions.rows, + columns: this._maximumDimensions.columns + }; + } + + private readonly _onDidChangeMaximumDimensions: Emitter = new Emitter(); + public get onDidChangeMaximumDimensions(): Event { + return this._onDidChangeMaximumDimensions && this._onDidChangeMaximumDimensions.event; + } + + public get terminal(): ExtHostTerminal { + return this._terminal; + } + + constructor( + proxy: MainThreadTerminalServiceShape, + private _name: string, + private _terminal: ExtHostTerminal + ) { + super(proxy); + this._proxy.$createTerminalRenderer(this._name).then(id => { + this._runQueuedRequests(id); + (this._terminal)._runQueuedRequests(id); + }); + } + + public write(data: string): void { + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererWrite, [data]); + } + + public _fireOnInput(data: string): void { + this._onInput.fire(data); + } + + public _setMaximumDimensions(columns: number, rows: number): void { + if (this._maximumDimensions && this._maximumDimensions.columns === columns && this._maximumDimensions.rows === rows) { + return; + } + this._maximumDimensions = { columns, rows }; + this._onDidChangeMaximumDimensions.fire(this.maximumDimensions); } } export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _proxy: MainThreadTerminalServiceShape; + private _activeTerminal: ExtHostTerminal; private _terminals: ExtHostTerminal[] = []; - private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; + private _terminalProcesses: { [id: number]: TerminalProcess } = {}; + private _terminalRenderers: ExtHostTerminalRenderer[] = []; + public get activeTerminal(): ExtHostTerminal { return this._activeTerminal; } public get terminals(): ExtHostTerminal[] { return this._terminals; } private readonly _onDidCloseTerminal: Emitter = new Emitter(); public get onDidCloseTerminal(): Event { return this._onDidCloseTerminal && this._onDidCloseTerminal.event; } private readonly _onDidOpenTerminal: Emitter = new Emitter(); public get onDidOpenTerminal(): Event { return this._onDidOpenTerminal && this._onDidOpenTerminal.event; } + private readonly _onDidChangeActiveTerminal: Emitter = new Emitter(); + public get onDidChangeActiveTerminal(): Event { return this._onDidChangeActiveTerminal && this._onDidChangeActiveTerminal.event; } constructor( mainContext: IMainContext, @@ -143,53 +246,108 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, name); + const terminal = new ExtHostTerminal(this._proxy, name); terminal.create(shellPath, shellArgs); this._terminals.push(terminal); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, options.name); + const terminal = new ExtHostTerminal(this._proxy, options.name); terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env /*, options.waitOnExit*/); this._terminals.push(terminal); return terminal; } - public $acceptTerminalProcessData(id: number, data: string): void { - let index = this._getTerminalIndexById(id); - if (index === null) { - return; + public createTerminalRenderer(name: string): vscode.TerminalRenderer { + const terminal = new ExtHostTerminal(this._proxy, name); + terminal._setProcessId(undefined); + this._terminals.push(terminal); + + const renderer = new ExtHostTerminalRenderer(this._proxy, name, terminal); + this._terminalRenderers.push(renderer); + + return renderer; + } + + public $acceptActiveTerminalChanged(id: number | null): void { + const original = this._activeTerminal; + if (id === null) { + this._activeTerminal = undefined; + if (original !== this._activeTerminal) { + this._onDidChangeActiveTerminal.fire(this._activeTerminal); + } + } + this._performTerminalIdAction(id, terminal => { + if (terminal) { + this._activeTerminal = terminal; + if (original !== this._activeTerminal) { + this._onDidChangeActiveTerminal.fire(this._activeTerminal); + } + } + }); + } + + public $acceptTerminalProcessData(id: number, data: string): void { + // TODO: Queue requests, currently the first 100ms of data may get missed + const terminal = this._getTerminalById(id); + if (terminal) { + terminal._fireOnData(data); + } + } + + public $acceptTerminalRendererDimensions(id: number, cols: number, rows: number): void { + const renderer = this._getTerminalRendererById(id); + if (renderer) { + renderer._setMaximumDimensions(cols, rows); + } + } + + public $acceptTerminalRendererInput(id: number, data: string): void { + const renderer = this._getTerminalRendererById(id); + if (renderer) { + renderer._fireOnInput(data); } - const terminal = this._terminals[index]; - terminal._fireOnData(data); } public $acceptTerminalClosed(id: number): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalObjectIndexById(this.terminals, id); if (index === null) { return; } - let terminal = this._terminals.splice(index, 1)[0]; + const terminal = this._terminals.splice(index, 1)[0]; this._onDidCloseTerminal.fire(terminal); } public $acceptTerminalOpened(id: number, name: string): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalObjectIndexById(this._terminals, id); if (index !== null) { // The terminal has already been created (via createTerminal*), only fire the event this._onDidOpenTerminal.fire(this.terminals[index]); return; } - let terminal = new ExtHostTerminal(this._proxy, name, id); + const renderer = this._getTerminalRendererById(id); + const terminal = new ExtHostTerminal(this._proxy, name, id, renderer ? RENDERER_NO_PROCESS_ID : undefined); this._terminals.push(terminal); this._onDidOpenTerminal.fire(terminal); } public $acceptTerminalProcessId(id: number, processId: number): void { + this._performTerminalIdAction(id, terminal => terminal._setProcessId(processId)); + } + + private _performTerminalIdAction(id: number, callback: (terminal: ExtHostTerminal) => void): void { let terminal = this._getTerminalById(id); if (terminal) { - terminal._setProcessId(processId); + callback(terminal); + } else { + // Retry one more time in case the terminal has not yet been initialized. + setTimeout(() => { + terminal = this._getTerminalById(id); + if (terminal) { + callback(terminal); + } + }, EXT_HOST_CREATION_DELAY); } } @@ -199,7 +357,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { const terminalConfig = this._extHostConfiguration.getConfiguration('terminal.integrated'); - const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; if (!shellLaunchConfig.executable) { // TODO: This duplicates some of TerminalConfigHelper.mergeDefaultShellPathAndArgs and should be merged // this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); @@ -213,7 +370,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } // TODO: Base the cwd on the last active workspace root - // const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + // const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); // this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); const initialCwd = os.homedir(); @@ -223,61 +380,48 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); - // shellLaunchConfig.env = envFromShell; // Merge process env with the env from config - const parentEnv = { ...process.env }; - // terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + const env = { ...process.env }; + // terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); // Continue env initialization, merging in the env from the launch // config and adding keys that are needed to create the process - const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows); - let cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; - const options = { env, cwd, execArgv: [] }; + const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); // Fork the process and listen for messages - this._logService.debug(`Terminal process launching on ext host`, options); - this._terminalProcesses[id] = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); - this._terminalProcesses[id].on('message', (message: IMessageFromTerminalProcess) => { - switch (message.type) { - case 'pid': this._proxy.$sendProcessPid(id, message.content); break; - case 'title': this._proxy.$sendProcessTitle(id, message.content); break; - case 'data': this._proxy.$sendProcessData(id, message.content); break; - } - }); - this._terminalProcesses[id].on('exit', (exitCode) => this._onProcessExit(id, exitCode)); + this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); + this._terminalProcesses[id] = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env); + 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)); } public $acceptProcessInput(id: number, data: string): void { - if (this._terminalProcesses[id].connected) { - this._terminalProcesses[id].send({ event: 'input', data }); - } + this._terminalProcesses[id].input(data); } public $acceptProcessResize(id: number, cols: number, rows: number): void { - if (this._terminalProcesses[id].connected) { - try { - this._terminalProcesses[id].send({ event: 'resize', cols, rows }); - } catch (error) { - // We tried to write to a closed pipe / channel. - if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { - throw (error); - } + try { + this._terminalProcesses[id].resize(cols, rows); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); } } } - public $acceptProcessShutdown(id: number): void { - if (this._terminalProcesses[id].connected) { - this._terminalProcesses[id].send({ event: 'shutdown' }); - } + public $acceptProcessShutdown(id: number, immediate: boolean): void { + this._terminalProcesses[id].shutdown(immediate); } private _onProcessExit(id: number, exitCode: number): void { // Remove listeners - const process = this._terminalProcesses[id]; - process.removeAllListeners('message'); - process.removeAllListeners('exit'); + this._terminalProcesses[id].dispose(); // Remove process reference delete this._terminalProcesses[id]; @@ -286,16 +430,43 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._proxy.$sendProcessExit(id, exitCode); } - private _getTerminalById(id: number): ExtHostTerminal { - let index = this._getTerminalIndexById(id); - return index !== null ? this._terminals[index] : null; + private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { + return new Promise(c => { + if (retries === 0) { + c(undefined); + return; + } + + const terminal = this._getTerminalById(id); + if (terminal) { + c(terminal); + } else { + // This should only be needed immediately after createTerminalRenderer is called as + // the ExtHostTerminal has not yet been iniitalized + setTimeout(() => { + c(this._getTerminalByIdEventually(id, retries - 1)); + }, 200); + } + }); } - private _getTerminalIndexById(id: number): number { + private _getTerminalById(id: number): ExtHostTerminal { + return this._getTerminalObjectById(this._terminals, id); + } + + private _getTerminalRendererById(id: number): ExtHostTerminalRenderer { + return this._getTerminalObjectById(this._terminalRenderers, id); + } + + private _getTerminalObjectById(array: T[], id: number): T { + const index = this._getTerminalObjectIndexById(array, id); + return index !== null ? array[index] : null; + } + + private _getTerminalObjectIndexById(array: T[], id: number): number { let index: number = null; - this._terminals.some((terminal, i) => { - // TODO: This shouldn't be cas - let thisId = (terminal)._id; + array.some((item, i) => { + const thisId = item._id; if (thisId === id) { index = i; return true; diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/node/extHostTextEditor.ts index 0d9d7c2d9a7..5131242cf21 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -322,6 +322,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { private _visibleRanges: Range[]; private _viewColumn: vscode.ViewColumn; private _disposed: boolean = false; + private _hasDecorationsForKey: { [key: string]: boolean; }; get id(): string { return this._id; } @@ -337,6 +338,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { this._options = new ExtHostTextEditorOptions(this._proxy, this._id, options); this._visibleRanges = visibleRanges; this._viewColumn = viewColumn; + this._hasDecorationsForKey = Object.create(null); } dispose() { @@ -436,6 +438,16 @@ export class ExtHostTextEditor implements vscode.TextEditor { } setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { + const willBeEmpty = (ranges.length === 0); + if (willBeEmpty && !this._hasDecorationsForKey[decorationType.key]) { + // avoid no-op call to the renderer + return; + } + if (willBeEmpty) { + delete this._hasDecorationsForKey[decorationType.key]; + } else { + this._hasDecorationsForKey[decorationType.key] = true; + } this._runOnProxy( () => { if (TypeConverters.isDecorationOptionsArr(ranges)) { @@ -473,7 +485,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { ); } - private _trySetSelection(): TPromise { + private _trySetSelection(): Thenable { let selection = this._selections.map(TypeConverters.Selection.from); return this._runOnProxy(() => this._proxy.$trySetSelections(this._id, selection)); } @@ -494,9 +506,14 @@ export class ExtHostTextEditor implements vscode.TextEditor { return this._applyEdit(edit); } - private _applyEdit(editBuilder: TextEditorEdit): TPromise { + private _applyEdit(editBuilder: TextEditorEdit): Thenable { let editData = editBuilder.finalize(); + // return when there is nothing to do + if (editData.edits.length === 0 && !editData.setEndOfLine) { + return TPromise.wrap(true); + } + // check that the edits are not overlapping (i.e. illegal) let editRanges = editData.edits.map(edit => edit.range); @@ -575,7 +592,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { // ---- util - private _runOnProxy(callback: () => TPromise): TPromise { + private _runOnProxy(callback: () => Thenable): Thenable { if (this._disposed) { console.warn('TextEditor is closed/disposed'); return TPromise.as(undefined); @@ -589,7 +606,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { } } -function warnOnError(promise: TPromise): void { +function warnOnError(promise: Thenable): void { promise.then(null, (err) => { console.warn(err); }); diff --git a/src/vs/workbench/api/node/extHostTextEditors.ts b/src/vs/workbench/api/node/extHostTextEditors.ts index 4bbe0c52412..d1c332244af 100644 --- a/src/vs/workbench/api/node/extHostTextEditors.ts +++ b/src/vs/workbench/api/node/extHostTextEditors.ts @@ -6,12 +6,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { toThenable } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; import { TextEditorSelectionChangeKind } from './extHostTypes'; import * as TypeConverters from './extHostTypeConverters'; import { TextEditorDecorationType, ExtHostTextEditor } from './extHostTextEditor'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; -import { MainContext, MainThreadTextEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IMainContext, WorkspaceEditDto, IEditorPropertiesChangeData } from './extHost.protocol'; +import { MainContext, MainThreadTextEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IMainContext, IEditorPropertiesChangeData } from './extHost.protocol'; import * as vscode from 'vscode'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -53,10 +52,10 @@ export class ExtHostEditors implements ExtHostEditorsShape { return this._extHostDocumentsAndEditors.allEditors(); } - showTextDocument(document: vscode.TextDocument, column: vscode.ViewColumn, preserveFocus: boolean): TPromise; - showTextDocument(document: vscode.TextDocument, options: { column: vscode.ViewColumn, preserveFocus: boolean, pinned: boolean }): TPromise; - showTextDocument(document: vscode.TextDocument, columnOrOptions: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): TPromise; - showTextDocument(document: vscode.TextDocument, columnOrOptions: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): TPromise { + showTextDocument(document: vscode.TextDocument, column: vscode.ViewColumn, preserveFocus: boolean): Thenable; + showTextDocument(document: vscode.TextDocument, options: { column: vscode.ViewColumn, preserveFocus: boolean, pinned: boolean }): Thenable; + showTextDocument(document: vscode.TextDocument, columnOrOptions: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): Thenable; + showTextDocument(document: vscode.TextDocument, columnOrOptions: vscode.ViewColumn | vscode.TextDocumentShowOptions, preserveFocus?: boolean): Thenable { let options: ITextDocumentShowOptions; if (typeof columnOrOptions === 'number') { options = { @@ -90,24 +89,8 @@ export class ExtHostEditors implements ExtHostEditorsShape { return new TextEditorDecorationType(this._proxy, options); } - applyWorkspaceEdit(edit: vscode.WorkspaceEdit): TPromise { - - const dto: WorkspaceEditDto = { edits: [] }; - - for (let entry of edit.entries()) { - let [uri, uriOrEdits] = entry; - if (Array.isArray(uriOrEdits)) { - let doc = this._extHostDocumentsAndEditors.getDocument(uri.toString()); - dto.edits.push({ - resource: uri, - modelVersionId: doc && doc.version, - edits: uriOrEdits.map(TypeConverters.TextEdit.from) - }); - // } else { - // dto.edits.push({ oldUri: uri, newUri: uriOrEdits }); - } - } - + applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Thenable { + const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); return this._proxy.$tryApplyWorkspaceEdit(dto); } diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index 499e859b46f..af5b5fd47e2 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -7,16 +7,18 @@ import { localize } from 'vs/nls'; import * as vscode from 'vscode'; import { basename } from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; import { ITreeItem, TreeViewItemHandleArg } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; -import { asWinJsPromise } from 'vs/base/common/async'; +import { asThenable } from 'vs/base/common/async'; import { TreeItemCollapsibleState, ThemeIcon } from 'vs/workbench/api/node/extHostTypes'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import { equals } from 'vs/base/common/arrays'; +import { ILogService } from 'vs/platform/log/common/log'; type TreeItemHandle = string; @@ -26,7 +28,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { constructor( private _proxy: MainThreadTreeViewsShape, - private commands: ExtHostCommands + private commands: ExtHostCommands, + private logService: ILogService ) { commands.registerArgumentProcessor({ processArgument: arg => { @@ -52,7 +55,10 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, get onDidExpandElement() { return treeView.onDidExpandElement; }, get selection() { return treeView.selectedElements; }, - reveal: (element: T, options?: { select?: boolean }): Thenable => { + get onDidChangeSelection() { return treeView.onDidChangeSelection; }, + get visible() { return treeView.visible; }, + get onDidChangeVisibility() { return treeView.onDidChangeVisibility; }, + reveal: (element: T, options?: { select?: boolean, focus?: boolean }): Thenable => { return treeView.reveal(element, options); }, dispose: () => { @@ -62,7 +68,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { }; } - $getChildren(treeViewId: string, treeItemHandle?: string): TPromise { + $getChildren(treeViewId: string, treeItemHandle?: string): Thenable { const treeView = this.treeViews.get(treeViewId); if (!treeView) { return TPromise.wrapError(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); @@ -86,8 +92,16 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { treeView.setSelection(treeItemHandles); } + $setVisible(treeViewId: string, isVisible: boolean): void { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId)); + } + treeView.setVisible(isVisible); + } + private createExtHostTreeViewer(id: string, dataProvider: vscode.TreeDataProvider): ExtHostTreeView { - const treeView = new ExtHostTreeView(id, dataProvider, this._proxy, this.commands.converter); + const treeView = new ExtHostTreeView(id, dataProvider, this._proxy, this.commands.converter, this.logService); this.treeViews.set(id, treeView); return treeView; } @@ -113,8 +127,11 @@ class ExtHostTreeView extends Disposable { private elements: Map = new Map(); private nodes: Map = new Map(); - private _selectedElements: T[] = []; - get selectedElements(): T[] { return this._selectedElements; } + private _visible: boolean = false; + get visible(): boolean { return this._visible; } + + private _selectedHandles: TreeItemHandle[] = []; + get selectedElements(): T[] { return this._selectedHandles.map(handle => this.getExtensionElement(handle)).filter(element => !isUndefinedOrNull(element)); } private _onDidExpandElement: Emitter> = this._register(new Emitter>()); readonly onDidExpandElement: Event> = this._onDidExpandElement.event; @@ -122,9 +139,15 @@ class ExtHostTreeView extends Disposable { private _onDidCollapseElement: Emitter> = this._register(new Emitter>()); readonly onDidCollapseElement: Event> = this._onDidCollapseElement.event; + private _onDidChangeSelection: Emitter> = this._register(new Emitter>()); + readonly onDidChangeSelection: Event> = this._onDidChangeSelection.event; + + private _onDidChangeVisibility: Emitter = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + private refreshPromise: TPromise = TPromise.as(null); - constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter) { + constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService) { super(); this.proxy.$registerTreeViewDataProvider(viewId); if (this.dataProvider.onDidChangeTreeData) { @@ -144,7 +167,7 @@ class ExtHostTreeView extends Disposable { } } - getChildren(parentHandle?: TreeItemHandle): TPromise { + getChildren(parentHandle?: TreeItemHandle): Thenable { const parentElement = parentHandle ? this.getExtensionElement(parentHandle) : void 0; if (parentHandle && !parentElement) { console.error(`No tree item with id \'${parentHandle}\' found.`); @@ -160,32 +183,46 @@ class ExtHostTreeView extends Disposable { return this.elements.get(treeItemHandle); } - reveal(element: T, options?: { select?: boolean }): TPromise { + reveal(element: T, options?: { select?: boolean, focus?: boolean }): TPromise { + options = options ? options : { select: true, focus: false }; + const select = isUndefinedOrNull(options.select) ? true : options.select; + const focus = isUndefinedOrNull(options.focus) ? false : options.focus; + if (typeof this.dataProvider.getParent !== 'function') { return TPromise.wrapError(new Error(`Required registered TreeDataProvider to implement 'getParent' method to access 'reveal' method`)); } return this.refreshPromise .then(() => this.resolveUnknownParentChain(element)) .then(parentChain => this.resolveTreeNode(element, parentChain[parentChain.length - 1]) - .then(treeNode => this.proxy.$reveal(this.viewId, treeNode.item, parentChain.map(p => p.item), options))); + .then(treeNode => this.proxy.$reveal(this.viewId, treeNode.item, parentChain.map(p => p.item), { select, focus })), error => this.logService.error(error)); } setExpanded(treeItemHandle: TreeItemHandle, expanded: boolean): void { const element = this.getExtensionElement(treeItemHandle); if (element) { if (expanded) { - this._onDidExpandElement.fire({ element }); + this._onDidExpandElement.fire(Object.freeze({ element })); } else { - this._onDidCollapseElement.fire({ element }); + this._onDidCollapseElement.fire(Object.freeze({ element })); } } } setSelection(treeItemHandles: TreeItemHandle[]): void { - this._selectedElements = treeItemHandles.map(handle => this.getExtensionElement(handle)).filter(element => !isUndefinedOrNull(element)); + if (!equals(this._selectedHandles, treeItemHandles)) { + this._selectedHandles = treeItemHandles; + this._onDidChangeSelection.fire(Object.freeze({ selection: this.selectedElements })); + } } - private resolveUnknownParentChain(element: T): TPromise { + setVisible(visible: boolean): void { + if (visible !== this._visible) { + this._visible = visible; + this._onDidChangeVisibility.fire(Object.freeze({ visible: this._visible })); + } + } + + private resolveUnknownParentChain(element: T): Thenable { return this.resolveParent(element) .then((parent) => { if (!parent) { @@ -200,16 +237,16 @@ class ExtHostTreeView extends Disposable { }); } - private resolveParent(element: T): TPromise { + private resolveParent(element: T): Thenable { const node = this.nodes.get(element); if (node) { return TPromise.as(node.parent ? this.elements.get(node.parent.item.handle) : null); } - return asWinJsPromise(() => this.dataProvider.getParent(element)); + return asThenable(() => this.dataProvider.getParent(element)); } - private resolveTreeNode(element: T, parent?: TreeNode): TPromise { - return asWinJsPromise(() => this.dataProvider.getTreeItem(element)) + private resolveTreeNode(element: T, parent?: TreeNode): Thenable { + return asThenable(() => this.dataProvider.getTreeItem(element)) .then(extTreeItem => this.createHandle(element, extTreeItem, parent, true)) .then(handle => this.getChildren(parent ? parent.item.handle : null) .then(() => { @@ -238,21 +275,21 @@ class ExtHostTreeView extends Disposable { return this.roots; } - private fetchChildrenNodes(parentElement?: T): TPromise { + private fetchChildrenNodes(parentElement?: T): Thenable { // clear children cache this.clearChildren(parentElement); const parentNode = parentElement ? this.nodes.get(parentElement) : void 0; - return asWinJsPromise(() => this.dataProvider.getChildren(parentElement)) + return asThenable(() => this.dataProvider.getChildren(parentElement)) .then(elements => TPromise.join( (elements || []) .filter(element => !!element) - .map(element => asWinJsPromise(() => this.dataProvider.getTreeItem(element)) + .map(element => asThenable(() => this.dataProvider.getTreeItem(element)) .then(extTreeItem => extTreeItem ? this.createAndRegisterTreeNode(element, extTreeItem, parentNode) : null)))) .then(nodes => nodes.filter(n => !!n)); } - private refresh(elements: T[]): TPromise { + private refresh(elements: T[]): Thenable { const hasRoot = elements.some(element => !element); if (hasRoot) { this.clearAll(); // clear cache @@ -308,11 +345,11 @@ class ExtHostTreeView extends Disposable { .then(() => Object.keys(itemsToRefresh).length ? this.proxy.$refresh(this.viewId, itemsToRefresh) : null); } - private refreshNode(treeItemHandle: TreeItemHandle): TPromise { + private refreshNode(treeItemHandle: TreeItemHandle): Thenable { const extElement = this.getExtensionElement(treeItemHandle); const existing = this.nodes.get(extElement); this.clearChildren(extElement); // clear children cache - return asWinJsPromise(() => this.dataProvider.getTreeItem(extElement)) + return asThenable(() => this.dataProvider.getTreeItem(extElement)) .then(extTreeItem => { if (extTreeItem) { const newNode = this.createTreeNode(extElement, extTreeItem, existing.parent); @@ -389,7 +426,7 @@ class ExtHostTreeView extends Disposable { return handle; } - private getLightIconPath(extensionTreeItem: vscode.TreeItem): string { + private getLightIconPath(extensionTreeItem: vscode.TreeItem): URI { if (extensionTreeItem.iconPath && !(extensionTreeItem.iconPath instanceof ThemeIcon)) { if (typeof extensionTreeItem.iconPath === 'string' || extensionTreeItem.iconPath instanceof URI) { @@ -400,18 +437,18 @@ class ExtHostTreeView extends Disposable { return void 0; } - private getDarkIconPath(extensionTreeItem: vscode.TreeItem): string { + private getDarkIconPath(extensionTreeItem: vscode.TreeItem): URI { if (extensionTreeItem.iconPath && !(extensionTreeItem.iconPath instanceof ThemeIcon) && extensionTreeItem.iconPath['dark']) { return this.getIconPath(extensionTreeItem.iconPath['dark']); } return void 0; } - private getIconPath(iconPath: string | URI): string { + private getIconPath(iconPath: string | URI): URI { if (iconPath instanceof URI) { - return iconPath.toString(); + return iconPath; } - return URI.file(iconPath).toString(); + return URI.file(iconPath); } private addNodeToCache(element: T, node: TreeNode): void { @@ -492,4 +529,4 @@ class ExtHostTreeView extends Disposable { dispose() { this.clearAll(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index b4d705fe4a8..777a7816f7f 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -6,13 +6,14 @@ import * as modes from 'vs/editor/common/modes'; import * as types from './extHostTypes'; +import * as search from 'vs/workbench/parts/search/common/search'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; import * as vscode from 'vscode'; -import URI from 'vs/base/common/uri'; -import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; +import { URI } from 'vs/base/common/uri'; +import { ProgressLocation as MainProgressLocation } from 'vs/workbench/services/progress/common/progress'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; @@ -20,8 +21,10 @@ import { ISelection } from 'vs/editor/common/core/selection'; import * as htmlContent from 'vs/base/common/htmlContent'; import { IRelativePattern } from 'vs/base/common/glob'; import * as languageSelector from 'vs/editor/common/modes/languageSelector'; -import { WorkspaceEditDto, ResourceTextEditDto } from 'vs/workbench/api/node/extHost.protocol'; +import { WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { MarkerSeverity, IRelatedInformation, IMarkerData, MarkerTag } from 'vs/platform/markers/common/markers'; +import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; export interface PositionLike { line: number; @@ -108,7 +111,7 @@ export namespace Diagnostic { code: String(value.code), severity: DiagnosticSeverity.from(value.severity), relatedInformation: value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformation.from), - customTags: Array.isArray(value.customTags) ? value.customTags.map(DiagnosticTag.from) : undefined, + tags: Array.isArray(value.tags) ? value.tags.map(DiagnosticTag.from) : undefined, }; } } @@ -158,29 +161,22 @@ export namespace DiagnosticSeverity { export namespace ViewColumn { export function from(column?: vscode.ViewColumn): EditorViewColumn { - let editorColumn: EditorViewColumn; - if (column === types.ViewColumn.One) { - editorColumn = 0; - } else if (column === types.ViewColumn.Two) { - editorColumn = 1; - } else if (column === types.ViewColumn.Three) { - editorColumn = 2; - } else { - // in any other case (no column or ViewColumn.Active), leave the - // editorColumn as undefined which signals to use the active column - editorColumn = undefined; + if (typeof column === 'number' && column >= types.ViewColumn.One) { + return column - 1; // adjust zero index (ViewColumn.ONE => 0) } - return editorColumn; + + if (column === types.ViewColumn.Beside) { + return SIDE_GROUP; + } + + return ACTIVE_GROUP; // default is always the active group } export function to(position?: EditorViewColumn): vscode.ViewColumn { - if (position === 0) { - return types.ViewColumn.One; - } else if (position === 1) { - return types.ViewColumn.Two; - } else if (position === 2) { - return types.ViewColumn.Three; + if (typeof position === 'number' && position >= 0) { + return position + 1; // adjust to index (ViewColumn.ONE => 1) } + return undefined; } } @@ -274,18 +270,19 @@ export const TextEdit = { }; export namespace WorkspaceEdit { - export function from(value: vscode.WorkspaceEdit): modes.WorkspaceEdit { - const result: modes.WorkspaceEdit = { + export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors): WorkspaceEditDto { + const result: WorkspaceEditDto = { edits: [] }; - for (const entry of value.entries()) { + for (const entry of (value as types.WorkspaceEdit)._allEntries()) { const [uri, uriOrEdits] = entry; if (Array.isArray(uriOrEdits)) { // text edits - result.edits.push({ resource: uri, edits: uriOrEdits.map(TextEdit.from) }); + let doc = documents ? documents.getDocument(uri.toString()) : undefined; + result.edits.push({ resource: uri, modelVersionId: doc && doc.version, edits: uriOrEdits.map(TextEdit.from) }); } else { // resource edits - result.edits.push({ oldUri: uri, newUri: uriOrEdits }); + result.edits.push({ oldUri: uri, newUri: uriOrEdits, options: entry[2] }); } } return result; @@ -299,11 +296,12 @@ export namespace WorkspaceEdit { URI.revive((edit).resource), (edit).edits.map(TextEdit.to) ); - // } else { - // result.renameResource( - // URI.revive((edit).oldUri), - // URI.revive((edit).newUri) - // ); + } else { + result.renameFile( + URI.revive((edit).oldUri), + URI.revive((edit).newUri), + (edit).options + ); } } return result; @@ -355,16 +353,16 @@ export namespace SymbolKind { } } -export namespace SymbolInformation { - export function from(info: vscode.SymbolInformation): modes.SymbolInformation { - return { +export namespace WorkspaceSymbol { + export function from(info: vscode.SymbolInformation): search.IWorkspaceSymbol { + return { name: info.name, kind: SymbolKind.from(info.kind), containerName: info.containerName, location: location.from(info.location) }; } - export function to(info: modes.SymbolInformation): types.SymbolInformation { + export function to(info: search.IWorkspaceSymbol): types.SymbolInformation { return new types.SymbolInformation( info.name, SymbolKind.to(info.kind), @@ -374,27 +372,27 @@ export namespace SymbolInformation { } } -export namespace SymbolInformation2 { - export function from(info: vscode.SymbolInformation2): modes.SymbolInformation { - let result: modes.SymbolInformation = { +export namespace DocumentSymbol { + export function from(info: vscode.DocumentSymbol): modes.DocumentSymbol { + let result: modes.DocumentSymbol = { name: info.name, - detail: undefined, - location: location.from(info.location), - definingRange: Range.from(info.definingRange), - kind: SymbolKind.from(info.kind), - containerName: info.containerName + detail: info.detail, + range: Range.from(info.range), + selectionRange: Range.from(info.selectionRange), + kind: SymbolKind.from(info.kind) }; if (info.children) { result.children = info.children.map(from); } return result; } - export function to(info: modes.SymbolInformation): vscode.SymbolInformation2 { - let result = new types.SymbolInformation2( + export function to(info: modes.DocumentSymbol): vscode.DocumentSymbol { + let result = new types.DocumentSymbol( info.name, + info.detail, SymbolKind.to(info.kind), - info.containerName, - location.to(info.location), + Range.to(info.range), + Range.to(info.selectionRange), ); if (info.children) { result.children = info.children.map(to) as any; @@ -415,6 +413,23 @@ export const location = { } }; +export namespace DefinitionLink { + export function from(value: vscode.Location | vscode.DefinitionLink): modes.DefinitionLink { + const definitionLink = value; + const location = value; + return { + origin: definitionLink.originSelectionRange + ? Range.from(definitionLink.originSelectionRange) + : undefined, + uri: definitionLink.targetUri ? definitionLink.targetUri : location.uri, + range: Range.from(definitionLink.targetRange ? definitionLink.targetRange : location.range), + selectionRange: definitionLink.targetSelectionRange + ? Range.from(definitionLink.targetSelectionRange) + : undefined, + }; + } +} + export namespace Hover { export function from(hover: vscode.Hover): modes.Hover { return { @@ -514,6 +529,8 @@ export namespace Suggest { result.documentation = htmlContent.isMarkdownString(suggestion.documentation) ? MarkdownString.to(suggestion.documentation) : suggestion.documentation; result.sortText = suggestion.sortText; result.filterText = suggestion.filterText; + result.preselect = suggestion.preselect; + result.commitCharacters = suggestion.commitCharacters; // 'overwrite[Before|After]'-logic let overwriteBefore = (typeof suggestion.overwriteBefore === 'number') ? suggestion.overwriteBefore : 0; diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 916de5d9fbd..a0ef74e64bf 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -6,13 +6,15 @@ import * as crypto from 'crypto'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { illegalArgument } from 'vs/base/common/errors'; import * as vscode from 'vscode'; import { isMarkdownString } from 'vs/base/common/htmlContent'; import { IRelativePattern } from 'vs/base/common/glob'; import { relative } from 'path'; import { startsWith } from 'vs/base/common/strings'; +import { values } from 'vs/base/common/map'; +import { coalesce, equals } from 'vs/base/common/arrays'; export class Disposable { @@ -492,38 +494,45 @@ export class TextEdit { } } + +export interface IFileOperationOptions { + overwrite?: boolean; + ignoreIfExists?: boolean; + ignoreIfNotExists?: boolean; + recursive?: boolean; +} + +export interface IFileOperation { + _type: 1; + from: URI; + to: URI; + options?: IFileOperationOptions; +} + +export interface IFileTextEdit { + _type: 2; + uri: URI; + edit: TextEdit; +} + export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _seqPool: number = 0; + private _edits = new Array(); - private _resourceEdits: { seq: number, from: URI, to: URI }[] = []; - private _textEdits = new Map(); + renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void { + this._edits.push({ _type: 1, from, to, options }); + } - // createResource(uri: vscode.Uri): void { - // this.renameResource(undefined, uri); - // } + createFile(uri: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }): void { + this._edits.push({ _type: 1, from: undefined, to: uri, options }); + } - // deleteResource(uri: vscode.Uri): void { - // this.renameResource(uri, undefined); - // } - - // renameResource(from: vscode.Uri, to: vscode.Uri): void { - // this._resourceEdits.push({ seq: this._seqPool++, from, to }); - // } - - // resourceEdits(): [vscode.Uri, vscode.Uri][] { - // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); - // } + deleteFile(uri: vscode.Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }): void { + this._edits.push({ _type: 1, from: uri, to: undefined, options }); + } replace(uri: URI, range: Range, newText: string): void { - let edit = new TextEdit(range, newText); - let array = this.get(uri); - if (array) { - array.push(edit); - } else { - array = [edit]; - } - this.set(uri, array); + this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText) }); } insert(resource: URI, position: Position, newText: string): void { @@ -535,55 +544,76 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } has(uri: URI): boolean { - return this._textEdits.has(uri.toString()); + for (const edit of this._edits) { + if (edit._type === 2 && edit.uri.toString() === uri.toString()) { + return true; + } + } + return false; } set(uri: URI, edits: TextEdit[]): void { - let data = this._textEdits.get(uri.toString()); - if (!data) { - data = { seq: this._seqPool++, uri, edits: [] }; - this._textEdits.set(uri.toString(), data); - } if (!edits) { - data.edits = undefined; + // remove all text edits for `uri` + for (let i = 0; i < this._edits.length; i++) { + const element = this._edits[i]; + if (element._type === 2 && element.uri.toString() === uri.toString()) { + this._edits[i] = undefined; + } + } + this._edits = coalesce(this._edits); } else { - data.edits = edits.slice(0); + // append edit to the end + for (const edit of edits) { + if (edit) { + this._edits.push({ _type: 2, uri, edit }); + } + } } } get(uri: URI): TextEdit[] { - if (!this._textEdits.has(uri.toString())) { + let res: TextEdit[] = []; + for (let candidate of this._edits) { + if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) { + res.push(candidate.edit); + } + } + if (res.length === 0) { return undefined; } - const { edits } = this._textEdits.get(uri.toString()); - return edits ? edits.slice() : undefined; + return res; } entries(): [URI, TextEdit[]][] { - const res: [URI, TextEdit[]][] = []; - this._textEdits.forEach(value => res.push([value.uri, value.edits])); - return res.slice(); + let textEdits = new Map(); + for (let candidate of this._edits) { + if (candidate._type === 2) { + let textEdit = textEdits.get(candidate.uri.toString()); + if (!textEdit) { + textEdit = [candidate.uri, []]; + textEdits.set(candidate.uri.toString(), textEdit); + } + textEdit[1].push(candidate.edit); + } + } + return values(textEdits); } - allEntries(): ([URI, TextEdit[]] | [URI, URI])[] { - return this.entries(); - // // use the 'seq' the we have assigned when inserting - // // the operation and use that order in the resulting - // // array - // const res: ([URI, TextEdit[]] | [URI, URI])[] = []; - // this._textEdits.forEach(value => { - // const { seq, uri, edits } = value; - // res[seq] = [uri, edits]; - // }); - // this._resourceEdits.forEach(value => { - // const { seq, from, to } = value; - // res[seq] = [from, to]; - // }); - // return res; + _allEntries(): ([URI, TextEdit[]] | [URI, URI, IFileOperationOptions])[] { + let res: ([URI, TextEdit[]] | [URI, URI, IFileOperationOptions])[] = []; + for (let edit of this._edits) { + if (edit._type === 1) { + res.push([edit.from, edit.to, edit.options]); + } else { + res.push([edit.uri, [edit.edit]]); + } + } + return res; } get size(): number { - return this._textEdits.size + this._resourceEdits.length; + return this.entries().length; } toJSON(): any { @@ -741,6 +771,18 @@ export class DiagnosticRelatedInformation { this.location = location; this.message = message; } + + static isEqual(a: DiagnosticRelatedInformation, b: DiagnosticRelatedInformation): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.message === b.message + && a.location.range.isEqual(b.location.range) + && a.location.uri.toString() === b.location.uri.toString(); + } } export class Diagnostic { @@ -751,7 +793,7 @@ export class Diagnostic { code: string | number; severity: DiagnosticSeverity; relatedInformation: DiagnosticRelatedInformation[]; - customTags?: DiagnosticTag[]; + tags?: DiagnosticTag[]; constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { this.range = range; @@ -768,6 +810,23 @@ export class Diagnostic { code: this.code, }; } + + static isEqual(a: Diagnostic, b: Diagnostic): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.message === b.message + && a.severity === b.severity + && a.code === b.code + && a.severity === b.severity + && a.source === b.source + && a.range.isEqual(b.range) + && equals(a.tags, b.tags) + && equals(a.relatedInformation, b.relatedInformation, DiagnosticRelatedInformation.isEqual); + } } export class Hover { @@ -881,18 +940,29 @@ export class SymbolInformation { } } -export class SymbolInformation2 extends SymbolInformation { - definingRange: Range; - children: SymbolInformation2[]; - constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { - super(name, kind, containerName, location); +export class DocumentSymbol { + name: string; + detail: string; + kind: SymbolKind; + range: Range; + selectionRange: Range; + children: DocumentSymbol[]; + constructor(name: string, detail: string, kind: SymbolKind, range: Range, selectionRange: Range) { + this.name = name; + this.detail = detail; + this.kind = kind; + this.range = range; + this.selectionRange = selectionRange; this.children = []; - this.definingRange = location.range; - } + if (!this.range.contains(this.selectionRange)) { + throw new Error('selectionRange must be contained in fullRange'); + } + } } + export enum CodeActionTrigger { Automatic = 1, Manual = 2, @@ -1023,6 +1093,12 @@ export class SignatureHelp { } } +export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3, +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, @@ -1062,7 +1138,7 @@ export enum CompletionItemKind { TypeParameter = 24 } -export class CompletionItem { +export class CompletionItem implements vscode.CompletionItem { label: string; kind: CompletionItemKind; @@ -1070,8 +1146,10 @@ export class CompletionItem { documentation: string | MarkdownString; sortText: string; filterText: string; + preselect: boolean; insertText: string | SnippetString; range: Range; + commitCharacters: string[]; textEdit: TextEdit; additionalTextEdits: TextEdit[]; command: vscode.Command; @@ -1089,6 +1167,7 @@ export class CompletionItem { documentation: this.documentation, sortText: this.sortText, filterText: this.filterText, + preselect: this.preselect, insertText: this.insertText, textEdit: this.textEdit }; @@ -1109,9 +1188,16 @@ export class CompletionList { export enum ViewColumn { Active = -1, + Beside = -2, One = 1, Two = 2, - Three = 3 + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9 } export enum StatusBarAlignment { @@ -1906,3 +1992,10 @@ export enum CommentThreadCollapsibleState { */ Expanded = 1 } + +export class QuickInputButtons { + + static readonly Back: vscode.QuickInputButton = { iconPath: 'back.svg' }; + + private constructor() { } +} diff --git a/src/vs/workbench/api/node/extHostUrls.ts b/src/vs/workbench/api/node/extHostUrls.ts index 534dfb4f02a..f8b7b37f9f4 100644 --- a/src/vs/workbench/api/node/extHostUrls.ts +++ b/src/vs/workbench/api/node/extHostUrls.ts @@ -5,9 +5,10 @@ import * as vscode from 'vscode'; import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from './extHost.protocol'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { toDisposable } from 'vs/base/common/lifecycle'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class ExtHostUrls implements ExtHostUrlsShape { @@ -15,7 +16,7 @@ export class ExtHostUrls implements ExtHostUrlsShape { private readonly _proxy: MainThreadUrlsShape; private handles = new Set(); - private handlers = new Map(); + private handlers = new Map(); constructor( mainContext: IMainContext @@ -23,7 +24,7 @@ export class ExtHostUrls implements ExtHostUrlsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadUrls); } - registerProtocolHandler(extensionId: string, handler: vscode.ProtocolHandler): vscode.Disposable { + registerUriHandler(extensionId: string, handler: vscode.UriHandler): vscode.Disposable { if (this.handles.has(extensionId)) { throw new Error(`Protocol handler already registered for extension ${extensionId}`); } @@ -31,23 +32,27 @@ export class ExtHostUrls implements ExtHostUrlsShape { const handle = ExtHostUrls.HandlePool++; this.handles.add(extensionId); this.handlers.set(handle, handler); - this._proxy.$registerProtocolHandler(handle, extensionId); + this._proxy.$registerUriHandler(handle, extensionId); return toDisposable(() => { this.handles.delete(extensionId); this.handlers.delete(handle); - this._proxy.$unregisterProtocolHandler(handle); + this._proxy.$unregisterUriHandler(handle); }); } - $handleExternalUri(handle: number, uri: UriComponents): TPromise { + $handleExternalUri(handle: number, uri: UriComponents): Thenable { const handler = this.handlers.get(handle); if (!handler) { return TPromise.as(null); } + try { + handler.handleUri(URI.revive(uri)); + } catch (err) { + onUnexpectedError(err); + } - handler.handleUri(URI.revive(uri)); return TPromise.as(null); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index ecaf9a5c215..ee366c67c41 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewPanelHandle } from './extHost.protocol'; -import * as vscode from 'vscode'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; -import { TPromise } from 'vs/base/common/winjs.base'; +import * as vscode from 'vscode'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol'; import { Disposable } from './extHostTypes'; -import URI from 'vs/base/common/uri'; + +type IconPath = URI | { light: URI, dark: URI }; export class ExtHostWebview implements vscode.Webview { private readonly _handle: WebviewPanelHandle; @@ -19,7 +21,7 @@ export class ExtHostWebview implements vscode.Webview { private _options: vscode.WebviewOptions; private _isDisposed: boolean = false; - readonly _onMessageEmitter = new Emitter(); + public readonly _onMessageEmitter = new Emitter(); public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; constructor( @@ -32,16 +34,16 @@ export class ExtHostWebview implements vscode.Webview { this._options = options; } - dispose() { + public dispose() { this._onMessageEmitter.dispose(); } - get html(): string { + public get html(): string { this.assertNotDisposed(); return this._html; } - set html(value: string) { + public set html(value: string) { this.assertNotDisposed(); if (this._html !== value) { this._html = value; @@ -49,11 +51,17 @@ export class ExtHostWebview implements vscode.Webview { } } - get options(): vscode.WebviewOptions { + public get options(): vscode.WebviewOptions { this.assertNotDisposed(); return this._options; } + public set options(newOptions: vscode.WebviewOptions) { + this.assertNotDisposed(); + this._proxy.$setOptions(this._handle, newOptions); + this._options = newOptions; + } + public postMessage(message: any): Thenable { this.assertNotDisposed(); return this._proxy.$postMessage(this._handle, message); @@ -72,12 +80,14 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { private readonly _proxy: MainThreadWebviewsShape; private readonly _viewType: string; private _title: string; + private _iconPath: IconPath; private readonly _options: vscode.WebviewPanelOptions; private readonly _webview: ExtHostWebview; private _isDisposed: boolean = false; private _viewColumn: vscode.ViewColumn; private _visible: boolean = true; + private _active: boolean = true; readonly _onDisposeEmitter = new Emitter(); public readonly onDidDispose: Event = this._onDisposeEmitter.event; @@ -143,6 +153,20 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { } } + get iconPath(): IconPath | undefined { + this.assertNotDisposed(); + return this._iconPath; + } + + set iconPath(value: IconPath | undefined) { + this.assertNotDisposed(); + if (this._iconPath !== value) { + this._iconPath = value; + + this._proxy.$setIconPath(this._handle, URI.isUri(value) ? { light: value, dark: value } : value); + } + } + get options() { return this._options; } @@ -157,7 +181,17 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { this._viewColumn = value; } - get visible(): boolean { + public get active(): boolean { + this.assertNotDisposed(); + return this._active; + } + + _setActive(value: boolean) { + this.assertNotDisposed(); + this._active = value; + } + + public get visible(): boolean { this.assertNotDisposed(); return this._visible; } @@ -174,9 +208,10 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { this.assertNotDisposed(); - this._proxy.$reveal(this._handle, - viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, - !!preserveFocus); + this._proxy.$reveal(this._handle, { + viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, + preserveFocus: !!preserveFocus + }); } private assertNotDisposed() { @@ -189,8 +224,11 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel { export class ExtHostWebviews implements ExtHostWebviewsShape { private static webviewHandlePool = 1; - private readonly _proxy: MainThreadWebviewsShape; + private static newHandle(): WebviewPanelHandle { + return ExtHostWebviews.webviewHandlePool++ + ''; + } + private readonly _proxy: MainThreadWebviewsShape; private readonly _webviewPanels = new Map(); private readonly _serializers = new Map(); @@ -200,22 +238,20 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews); } - createWebview( + public createWebview( + extensionLocation: URI, viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, - options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) | undefined, - extensionLocation: URI + options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, ): vscode.WebviewPanel { - options = options || {}; - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; const webviewShowOptions = { viewColumn: typeConverters.ViewColumn.from(viewColumn), preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus }; - const handle = ExtHostWebviews.webviewHandlePool++ + ''; + const handle = ExtHostWebviews.newHandle(); this._proxy.$createWebviewPanel(handle, viewType, title, webviewShowOptions, options, extensionLocation); const webview = new ExtHostWebview(handle, this._proxy, options); @@ -224,7 +260,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return panel; } - registerWebviewPanelSerializer( + public registerWebviewPanelSerializer( viewType: string, serializer: vscode.WebviewPanelSerializer ): vscode.Disposable { @@ -241,19 +277,26 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { }); } - $onMessage(handle: WebviewPanelHandle, message: any): void { + public $onMessage( + handle: WebviewPanelHandle, + message: any + ): void { const panel = this.getWebviewPanel(handle); if (panel) { panel.webview._onMessageEmitter.fire(message); } } - $onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, visible: boolean, position: EditorViewColumn): void { + public $onDidChangeWebviewPanelViewState( + handle: WebviewPanelHandle, + newState: WebviewPanelViewState + ): void { const panel = this.getWebviewPanel(handle); if (panel) { - const viewColumn = typeConverters.ViewColumn.to(position); - if (panel.visible !== visible || panel.viewColumn !== viewColumn) { - panel._setVisible(visible); + const viewColumn = typeConverters.ViewColumn.to(newState.position); + if (panel.active !== newState.active || panel.visible !== newState.visible || panel.viewColumn !== viewColumn) { + panel._setActive(newState.active); + panel._setVisible(newState.visible); panel._setViewColumn(viewColumn); panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel }); } diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 7fd58a461ac..26f0f0bf0bb 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -4,22 +4,27 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; -import { normalize } from 'vs/base/common/paths'; +import { join, relative } from 'path'; import { delta as arrayDelta } from 'vs/base/common/arrays'; -import { relative, posix } from 'path'; -import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext, MainThreadMessageServiceShape } from './extHost.protocol'; -import * as vscode from 'vscode'; -import { compare } from 'vs/base/common/strings'; +import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; -import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; +import { Counter } from 'vs/base/common/numbers'; +import { normalize } from 'vs/base/common/paths'; import { isLinux } from 'vs/base/common/platform'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { basenameOrAuthority, dirname, isEqual } from 'vs/base/common/resources'; +import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; -import { Severity } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IQueryOptions, IRawFileMatch2 } from 'vs/platform/search/common/search'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Range } from 'vs/workbench/api/node/extHostTypes'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; +import { ExtHostWorkspaceShape, IMainContext, IWorkspaceData, MainContext, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; +import { CancellationToken } from 'vs/base/common/cancellation'; function isFolderEqual(folderA: URI, folderB: URI): boolean { return isEqual(folderA, folderB, !isLinux); @@ -104,8 +109,8 @@ class ExtHostWorkspaceImpl extends Workspace { private readonly _workspaceFolders: vscode.WorkspaceFolder[] = []; private readonly _structure = TernarySearchTree.forPaths(); - private constructor(id: string, name: string, folders: vscode.WorkspaceFolder[]) { - super(id, name, folders.map(f => new WorkspaceFolder(f))); + private constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[]) { + super(id, folders.map(f => new WorkspaceFolder(f))); // setup the workspace folder data structure folders.forEach(folder => { @@ -114,6 +119,10 @@ class ExtHostWorkspaceImpl extends Workspace { }); } + get name(): string { + return this._name; + } + get workspaceFolders(): vscode.WorkspaceFolder[] { return this._workspaceFolders.slice(0); } @@ -121,7 +130,7 @@ class ExtHostWorkspaceImpl extends Workspace { getWorkspaceFolder(uri: URI, resolveParent?: boolean): vscode.WorkspaceFolder { if (resolveParent && this._structure.get(uri.toString())) { // `uri` is a workspace folder so we check for its parent - uri = uri.with({ path: posix.dirname(uri.path) }); + uri = dirname(uri); } return this._structure.findSubstr(uri.toString()); } @@ -133,8 +142,6 @@ class ExtHostWorkspaceImpl extends Workspace { export class ExtHostWorkspace implements ExtHostWorkspaceShape { - private static _requestIdPool = 0; - private readonly _onDidChangeWorkspace = new Emitter(); private readonly _proxy: MainThreadWorkspaceShape; @@ -145,10 +152,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; + private readonly _activeSearchCallbacks: ((match: IRawFileMatch2) => any)[] = []; + constructor( mainContext: IMainContext, data: IWorkspaceData, - private _logService: ILogService + private _logService: ILogService, + private _requestIdProvider: Counter ) { this._proxy = mainContext.getProxy(MainContext.MainThreadWorkspace); this._messageService = mainContext.getProxy(MainContext.MainThreadMessageService); @@ -161,6 +171,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { return this._actualWorkspace; } + get name(): string { + return this._actualWorkspace ? this._actualWorkspace.name : undefined; + } + private get _actualWorkspace(): ExtHostWorkspaceImpl { return this._unconfirmedWorkspace || this._confirmedWorkspace; } @@ -263,6 +277,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { if (folders.length === 0) { return undefined; } + // #54483 @Joh Why are we still using fsPath? return folders[0].uri.fsPath; } @@ -324,18 +339,16 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { // Events this._onDidChangeWorkspace.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) + added, + removed, })); } // --- search --- - findFiles(include: vscode.GlobPattern, exclude: vscode.GlobPattern, maxResults: number, extensionId: string, token?: vscode.CancellationToken): Thenable { + findFiles(include: vscode.GlobPattern, exclude: vscode.GlobPattern, maxResults: number, extensionId: string, token: vscode.CancellationToken = CancellationToken.None): Thenable { this._logService.trace(`extHostWorkspace#findFiles: fileSearch, extension: ${extensionId}, entryPoint: findFiles`); - const requestId = ExtHostWorkspace._requestIdPool++; - let includePattern: string; let includeFolder: string; if (include) { @@ -358,11 +371,80 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } } - const result = this._proxy.$startSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, requestId); - if (token) { - token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId)); + if (token && token.isCancellationRequested) { + return TPromise.wrap([]); + } + + return this._proxy.$startFileSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, token) + .then(data => Array.isArray(data) ? data.map(URI.revive) : []); + } + + findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, extensionId: string, token: vscode.CancellationToken = CancellationToken.None) { + this._logService.trace(`extHostWorkspace#findTextInFiles: textSearch, extension: ${extensionId}, entryPoint: findTextInFiles`); + + if (options.previewOptions && options.previewOptions.totalChars <= options.previewOptions.leadingChars) { + throw new Error('findTextInFiles: previewOptions.totalChars must be > previewOptions.leadingChars'); + } + + const requestId = this._requestIdProvider.getNext(); + + const globPatternToString = (pattern: vscode.GlobPattern | string) => { + if (typeof pattern === 'string') { + return pattern; + } + + return join(pattern.base, pattern.pattern); + }; + + const queryOptions: IQueryOptions = { + ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined, + disregardIgnoreFiles: typeof options.useIgnoreFiles === 'boolean' ? !options.useIgnoreFiles : undefined, + disregardExcludeSettings: options.exclude === null, + fileEncoding: options.encoding, + maxResults: options.maxResults, + previewOptions: options.previewOptions, + + includePattern: options.include && globPatternToString(options.include), + excludePattern: options.exclude && globPatternToString(options.exclude) + }; + + let isCanceled = false; + + this._activeSearchCallbacks[requestId] = p => { + if (isCanceled) { + return; + } + + p.matches.forEach(match => { + callback({ + uri: URI.revive(p.resource), + preview: { + text: match.preview.text, + match: new Range(match.preview.match.startLineNumber, match.preview.match.startColumn, match.preview.match.endLineNumber, match.preview.match.endColumn) + }, + range: new Range(match.range.startLineNumber, match.range.startColumn, match.range.endLineNumber, match.range.endColumn) + }); + }); + }; + + if (token.isCancellationRequested) { + return TPromise.wrap(undefined); + } + + return this._proxy.$startTextSearch(query, queryOptions, requestId, token).then( + () => { + delete this._activeSearchCallbacks[requestId]; + }, + err => { + delete this._activeSearchCallbacks[requestId]; + return TPromise.wrapError(err); + }); + } + + $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void { + if (this._activeSearchCallbacks[requestId]) { + this._activeSearchCallbacks[requestId](result); } - return result.then(data => Array.isArray(data) ? data.map(URI.revive) : []); } saveAll(includeUntitled?: boolean): Thenable { diff --git a/src/vs/workbench/api/shared/editor.ts b/src/vs/workbench/api/shared/editor.ts index 7c537a8edd0..6faf61d1063 100644 --- a/src/vs/workbench/api/shared/editor.ts +++ b/src/vs/workbench/api/shared/editor.ts @@ -9,18 +9,14 @@ import { IEditorGroupsService, IEditorGroup, GroupsOrder } from 'vs/workbench/se import { GroupIdentifier } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -// TODO@api this was previously a hardcoded list of editor positions (ONE, TWO, THREE) -// that with the introduction of grid editor feature is now unbounded. This should be -// revisited when the grid functionality is exposed to extensions, - export type EditorViewColumn = number; export function viewColumnToEditorGroup(editorGroupService: IEditorGroupsService, position?: EditorViewColumn): GroupIdentifier { - if (typeof position !== 'number') { - return ACTIVE_GROUP; // prefer active group when position is undefined + if (typeof position !== 'number' || position === ACTIVE_GROUP) { + return ACTIVE_GROUP; // prefer active group when position is undefined or passed in as such } - const groups = editorGroupService.getGroups(GroupsOrder.CREATION_TIME); + const groups = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); let candidate = groups[position]; if (candidate) { @@ -29,14 +25,14 @@ export function viewColumnToEditorGroup(editorGroupService: IEditorGroupsService let firstGroup = groups[0]; if (groups.length === 1 && firstGroup.count === 0) { - return firstGroup.id; // first editor should always open in first group + return firstGroup.id; // first editor should always open in first group independent from position provided } - return SIDE_GROUP; // open to the side if group not found + return SIDE_GROUP; // open to the side if group not found or we are instructed to } export function editorGroupToViewColumn(editorGroupService: IEditorGroupsService, editorGroup: IEditorGroup | GroupIdentifier): EditorViewColumn { - const group = typeof editorGroup === 'number' ? editorGroupService.getGroup(editorGroup) : editorGroup; + const group = (typeof editorGroup === 'number') ? editorGroupService.getGroup(editorGroup) : editorGroup; - return editorGroupService.getGroups(GroupsOrder.CREATION_TIME).indexOf(group); + return editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).indexOf(group); } \ No newline at end of file diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index 7efd4ae6437..c03895bfb41 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -106,5 +106,7 @@ export interface TaskFilterDTO { } export interface TaskSystemInfoDTO { + scheme: string; + authority: string; platform: string; } \ No newline at end of file diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 8538ba3afdb..2f0aceb67e5 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -6,7 +6,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; -import * as types from 'vs/base/common/types'; import { Action, IAction } from 'vs/base/common/actions'; import { BaseActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; @@ -20,35 +19,35 @@ export class ActionBarContributor { /** * Returns true if this contributor has actions for the given context. */ - public hasActions(context: any): boolean { + hasActions(context: any): boolean { return false; } /** * Returns an array of primary actions in the given context. */ - public getActions(context: any): IAction[] { + getActions(context: any): IAction[] { return []; } /** * Returns true if this contributor has secondary actions for the given context. */ - public hasSecondaryActions(context: any): boolean { + hasSecondaryActions(context: any): boolean { return false; } /** * Returns an array of secondary actions in the given context. */ - public getSecondaryActions(context: any): IAction[] { + getSecondaryActions(context: any): IAction[] { return []; } /** * Can return a specific IActionItem to render the given action. */ - public getActionItem(context: any, action: Action): BaseActionItem { + getActionItem(context: any, action: Action): BaseActionItem { return null; } } @@ -80,7 +79,7 @@ export class ContributableActionProvider implements IActionProvider { }; } - public hasActions(tree: ITree, element: any): boolean { + hasActions(tree: ITree, element: any): boolean { const context = this.toContext(tree, element); const contributors = this.registry.getActionBarContributors(Scope.VIEWER); @@ -94,7 +93,7 @@ export class ContributableActionProvider implements IActionProvider { return false; } - public getActions(tree: ITree, element: any): TPromise { + getActions(tree: ITree, element: any): TPromise { const actions: IAction[] = []; const context = this.toContext(tree, element); @@ -110,7 +109,7 @@ export class ContributableActionProvider implements IActionProvider { return TPromise.as(prepareActions(actions)); } - public hasSecondaryActions(tree: ITree, element: any): boolean { + hasSecondaryActions(tree: ITree, element: any): boolean { const context = this.toContext(tree, element); const contributors = this.registry.getActionBarContributors(Scope.VIEWER); @@ -124,7 +123,7 @@ export class ContributableActionProvider implements IActionProvider { return false; } - public getSecondaryActions(tree: ITree, element: any): TPromise { + getSecondaryActions(tree: ITree, element: any): TPromise { const actions: IAction[] = []; const context = this.toContext(tree, element); @@ -140,7 +139,7 @@ export class ContributableActionProvider implements IActionProvider { return TPromise.as(prepareActions(actions)); } - public getActionItem(tree: ITree, element: any, action: Action): BaseActionItem { + getActionItem(tree: ITree, element: any, action: Action): BaseActionItem { const contributors = this.registry.getActionBarContributors(Scope.VIEWER); const context = this.toContext(tree, element); @@ -163,36 +162,6 @@ export function prepareActions(actions: IAction[]): IAction[] { return actions; } - // Patch order if not provided - let lastOrder = -1; - let orderOffset = 0; - for (let l = 0; l < actions.length; l++) { - const a = actions[l]; - if (types.isUndefinedOrNull(a.order)) { - a.order = ++lastOrder; - orderOffset++; - } else { - a.order += orderOffset; - } - - lastOrder = a.order; - } - - // Sort by order - actions = actions.sort((first: Action, second: Action) => { - const firstOrder = first.order; - const secondOrder = second.order; - if (firstOrder < secondOrder) { - return -1; - } - - if (firstOrder > secondOrder) { - return 1; - } - - return 0; - }); - // Clean up leading separators let firstIndexOfAction = -1; for (let i = 0; i < actions.length; i++) { @@ -279,7 +248,7 @@ class ActionBarRegistry implements IActionBarRegistry { private actionBarContributorInstances: { [scope: string]: ActionBarContributor[] } = Object.create(null); private instantiationService: IInstantiationService; - public setInstantiationService(service: IInstantiationService): void { + setInstantiationService(service: IInstantiationService): void { this.instantiationService = service; while (this.actionBarContributorConstructors.length > 0) { @@ -301,7 +270,7 @@ class ActionBarRegistry implements IActionBarRegistry { return this.actionBarContributorInstances[scope] || []; } - public getActionBarActionsForContext(scope: string, context: any): IAction[] { + getActionBarActionsForContext(scope: string, context: any): IAction[] { const actions: IAction[] = []; // Go through contributors for scope @@ -316,7 +285,7 @@ class ActionBarRegistry implements IActionBarRegistry { return actions; } - public getSecondaryActionBarActionsForContext(scope: string, context: any): IAction[] { + getSecondaryActionBarActionsForContext(scope: string, context: any): IAction[] { const actions: IAction[] = []; // Go through contributors @@ -331,7 +300,7 @@ class ActionBarRegistry implements IActionBarRegistry { return actions; } - public getActionItemForContext(scope: string, context: any, action: Action): BaseActionItem { + getActionItemForContext(scope: string, context: any, action: Action): BaseActionItem { const contributors = this.getContributors(scope); for (let i = 0; i < contributors.length; i++) { const contributor = contributors[i]; @@ -344,7 +313,7 @@ class ActionBarRegistry implements IActionBarRegistry { return null; } - public registerActionBarContributor(scope: string, ctor: IConstructorSignature0): void { + registerActionBarContributor(scope: string, ctor: IConstructorSignature0): void { if (!this.instantiationService) { this.actionBarContributorConstructors.push({ scope: scope, @@ -355,7 +324,7 @@ class ActionBarRegistry implements IActionBarRegistry { } } - public getActionBarContributors(scope: string): ActionBarContributor[] { + getActionBarContributors(scope: string): ActionBarContributor[] { return this.getContributors(scope).slice(0); } } diff --git a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts b/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts index 3018aeda0d8..9f504717d73 100644 --- a/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleActivityBarVisibility.ts @@ -8,15 +8,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; 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 { 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 { - public static readonly ID = 'workbench.action.toggleActivityBarVisibility'; - public static readonly LABEL = nls.localize('toggleActivityBar', "Toggle Activity Bar Visibility"); + static readonly ID = 'workbench.action.toggleActivityBarVisibility'; + static readonly LABEL = nls.localize('toggleActivityBar', "Toggle Activity Bar Visibility"); private static readonly activityBarVisibleKey = 'workbench.activityBar.visible'; @@ -31,7 +31,7 @@ export class ToggleActivityBarVisibilityAction extends Action { this.enabled = !!this.partService; } - public run(): TPromise { + run(): TPromise { const visibility = this.partService.isVisible(Parts.ACTIVITYBAR_PART); const newVisibilityValue = !visibility; @@ -40,4 +40,13 @@ export class ToggleActivityBarVisibilityAction extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, ToggleActivityBarVisibilityAction.LABEL), 'View: Toggle Activity Bar Visibility', nls.localize('view', "View")); \ No newline at end of file +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 index 663541555e4..4be449f24ac 100644 --- a/src/vs/workbench/browser/actions/toggleCenteredLayout.ts +++ b/src/vs/workbench/browser/actions/toggleCenteredLayout.ts @@ -7,14 +7,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +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 { - public static readonly ID = 'workbench.action.toggleCenteredLayout'; - public static readonly LABEL = nls.localize('toggleCenteredLayout', "Toggle Centered Layout"); + static readonly ID = 'workbench.action.toggleCenteredLayout'; + static readonly LABEL = nls.localize('toggleCenteredLayout', "Toggle Centered Layout"); constructor( id: string, @@ -25,7 +25,7 @@ class ToggleCenteredLayout extends Action { this.enabled = !!this.partService; } - public run(): TPromise { + run(): TPromise { this.partService.centerEditorLayout(!this.partService.isEditorLayoutCentered()); return TPromise.as(null); @@ -34,3 +34,21 @@ class ToggleCenteredLayout extends Action { 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 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutCentered', + title: nls.localize({ key: 'miCenteredEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered") + }, + order: 2 +}); diff --git a/src/vs/workbench/browser/actions/toggleEditorLayout.ts b/src/vs/workbench/browser/actions/toggleEditorLayout.ts index b65f251ae75..32b9cc1c921 100644 --- a/src/vs/workbench/browser/actions/toggleEditorLayout.ts +++ b/src/vs/workbench/browser/actions/toggleEditorLayout.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; 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 { 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'; @@ -19,8 +19,8 @@ import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/gr export class ToggleEditorLayoutAction extends Action { - public static readonly ID = 'workbench.action.toggleEditorGroupLayout'; - public static readonly LABEL = nls.localize('flipLayout', "Flip Editor Group Layout"); + static readonly ID = 'workbench.action.toggleEditorGroupLayout'; + static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"); private toDispose: IDisposable[]; @@ -48,14 +48,14 @@ export class ToggleEditorLayoutAction extends Action { this.enabled = this.editorGroupService.count > 1; } - public run(): TPromise { + run(): TPromise { const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; this.editorGroupService.setGroupOrientation(newOrientation); return TPromise.as(null); } - public dispose(): void { + dispose(): void { this.toDispose = dispose(this.toDispose); super.dispose(); @@ -73,4 +73,13 @@ CommandsRegistry.registerCommand('_workbench.editor.setGroupOrientation', functi 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); \ No newline at end of file +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 index b0a223a0ab6..3c64a5a1c6d 100644 --- a/src/vs/workbench/browser/actions/toggleSidebarPosition.ts +++ b/src/vs/workbench/browser/actions/toggleSidebarPosition.ts @@ -8,15 +8,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; 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 { 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 { - public static readonly ID = 'workbench.action.toggleSidebarPosition'; - public static readonly LABEL = nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"); + static readonly ID = 'workbench.action.toggleSidebarPosition'; + static readonly LABEL = nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"); private static readonly sidebarPositionConfigurationKey = 'workbench.sideBar.location'; @@ -31,7 +31,7 @@ export class ToggleSidebarPositionAction extends Action { this.enabled = !!this.partService && !!this.configurationService; } - public run(): TPromise { + run(): TPromise { const position = this.partService.getSideBarPosition(); const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; @@ -41,3 +41,12 @@ export class ToggleSidebarPositionAction extends Action { 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 index 72afba76346..e69151cac2b 100644 --- a/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleSidebarVisibility.ts @@ -8,15 +8,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; 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 { 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 { - public static readonly ID = 'workbench.action.toggleSidebarVisibility'; - public static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility"); + static readonly ID = 'workbench.action.toggleSidebarVisibility'; + static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility"); constructor( id: string, @@ -28,11 +28,20 @@ export class ToggleSidebarVisibilityAction extends Action { this.enabled = !!this.partService; } - public run(): TPromise { + run(): TPromise { const hideSidebar = this.partService.isVisible(Parts.SIDEBAR_PART); return this.partService.setSideBarHidden(hideSidebar); } } 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")); \ No newline at end of file +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 index da09591e2d0..2ec27e5f765 100644 --- a/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleStatusbarVisibility.ts @@ -8,15 +8,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; 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 { 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 { - public static readonly ID = 'workbench.action.toggleStatusbarVisibility'; - public static readonly LABEL = nls.localize('toggleStatusbar', "Toggle Status Bar Visibility"); + static readonly ID = 'workbench.action.toggleStatusbarVisibility'; + static readonly LABEL = nls.localize('toggleStatusbar', "Toggle Status Bar Visibility"); private static readonly statusbarVisibleKey = 'workbench.statusBar.visible'; @@ -31,7 +31,7 @@ export class ToggleStatusbarVisibilityAction extends Action { this.enabled = !!this.partService; } - public run(): TPromise { + run(): TPromise { const visibility = this.partService.isVisible(Parts.STATUSBAR_PART); const newVisibilityValue = !visibility; @@ -40,4 +40,13 @@ export class ToggleStatusbarVisibilityAction extends Action { } const registry = Registry.as(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, ToggleStatusbarVisibilityAction.LABEL), 'View: Toggle Status Bar Visibility', nls.localize('view', "View")); \ No newline at end of file +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 index 9bf5df9e7a3..ebfb647ffab 100644 --- a/src/vs/workbench/browser/actions/toggleTabsVisibility.ts +++ b/src/vs/workbench/browser/actions/toggleTabsVisibility.ts @@ -15,8 +15,8 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; export class ToggleTabsVisibilityAction extends Action { - public static readonly ID = 'workbench.action.toggleTabsVisibility'; - public static readonly LABEL = nls.localize('toggleTabs', "Toggle Tab Visibility"); + static readonly ID = 'workbench.action.toggleTabsVisibility'; + static readonly LABEL = nls.localize('toggleTabs', "Toggle Tab Visibility"); private static readonly tabsVisibleKey = 'workbench.editor.showTabs'; @@ -28,7 +28,7 @@ export class ToggleTabsVisibilityAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); const newVisibilityValue = !visibility; diff --git a/src/vs/workbench/browser/actions/toggleZenMode.ts b/src/vs/workbench/browser/actions/toggleZenMode.ts index 3d58623c10c..a13035026da 100644 --- a/src/vs/workbench/browser/actions/toggleZenMode.ts +++ b/src/vs/workbench/browser/actions/toggleZenMode.ts @@ -8,14 +8,14 @@ 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 } from 'vs/platform/actions/common/actions'; +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 { - public static readonly ID = 'workbench.action.toggleZenMode'; - public static readonly LABEL = nls.localize('toggleZenMode', "Toggle Zen Mode"); + static readonly ID = 'workbench.action.toggleZenMode'; + static readonly LABEL = nls.localize('toggleZenMode', "Toggle Zen Mode"); constructor( id: string, @@ -26,11 +26,20 @@ class ToggleZenMode extends Action { this.enabled = !!this.partService; } - public run(): TPromise { + run(): TPromise { this.partService.toggleZenMode(); return TPromise.as(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")); \ No newline at end of file +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 a916997520c..b7b97385cda 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -19,6 +19,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ICommandService } from 'vs/platform/commands/common/commands'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID, defaultWorkspacePath, defaultFilePath, defaultFolderPath } from 'vs/workbench/browser/actions/workspaceCommands'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; export class OpenFileAction extends Action { @@ -36,7 +38,8 @@ export class OpenFileAction extends Action { } run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickFileAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultFilePath(this.contextService, this.historyService) } }); + const defaultPathURI = defaultFilePath(this.contextService, this.historyService, Schemas.file); + return this.windowService.pickFileAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } } @@ -56,7 +59,8 @@ export class OpenFolderAction extends Action { } run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickFolderAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultFolderPath(this.contextService, this.historyService) } }); + const defaultPathURI = defaultFolderPath(this.contextService, this.historyService, Schemas.file); + return this.windowService.pickFolderAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } } @@ -76,7 +80,8 @@ export class OpenFileFolderAction extends Action { } run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickFileFolderAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultFilePath(this.contextService, this.historyService) } }); + const defaultPathURI = defaultFilePath(this.contextService, this.historyService, Schemas.file); + return this.windowService.pickFileFolderAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } } @@ -93,7 +98,7 @@ export class AddRootFolderAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); } } @@ -113,7 +118,7 @@ export class GlobalRemoveRootFolderAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const state = this.contextService.getWorkbenchState(); // Workspace / Folder @@ -148,7 +153,7 @@ export class SaveWorkspaceAsAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.getNewWorkspaceConfigPath().then(configPath => { if (configPath) { switch (this.contextService.getWorkbenchState()) { @@ -167,11 +172,12 @@ export class SaveWorkspaceAsAction extends Action { } private getNewWorkspaceConfigPath(): TPromise { + const defaultPathURI = defaultWorkspacePath(this.contextService, this.historyService, this.environmentService, Schemas.file); return this.windowService.showSaveDialog({ buttonLabel: mnemonicButtonLabel(nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), title: nls.localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, - defaultPath: defaultWorkspacePath(this.contextService, this.historyService, this.environmentService) + defaultPath: defaultPathURI && defaultPathURI.fsPath }); } } @@ -192,15 +198,16 @@ export class OpenWorkspaceAction extends Action { super(id, label); } - public run(event?: any, data?: ITelemetryData): TPromise { - return this.windowService.pickWorkspaceAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultWorkspacePath(this.contextService, this.historyService, this.environmentService) } }); + run(event?: any, data?: ITelemetryData): TPromise { + const defaultPathURI = defaultWorkspacePath(this.contextService, this.historyService, this.environmentService, Schemas.file); + return this.windowService.pickWorkspaceAndOpen({ telemetryExtraData: data, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } } export class OpenWorkspaceConfigFileAction extends Action { - public static readonly ID = 'workbench.action.openWorkspaceConfigFile'; - public static readonly LABEL = nls.localize('openWorkspaceConfigFile', "Open Workspace Configuration File"); + static readonly ID = 'workbench.action.openWorkspaceConfigFile'; + static readonly LABEL = nls.localize('openWorkspaceConfigFile', "Open Workspace Configuration File"); constructor( id: string, @@ -213,15 +220,15 @@ export class OpenWorkspaceConfigFileAction extends Action { this.enabled = !!this.workspaceContextService.getWorkspace().configuration; } - public run(): TPromise { + run(): TPromise { return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration }); } } export class DuplicateWorkspaceInNewWindowAction extends Action { - public static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - public static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window"); + static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; + static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window"); constructor( id: string, @@ -234,12 +241,12 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const folders = this.workspaceContextService.getWorkspace().folders; return this.workspacesService.createWorkspace(folders).then(newWorkspace => { return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { - return this.windowService.openWindow([newWorkspace.configPath], { forceNewWindow: true }); + return this.windowService.openWindow([URI.file(newWorkspace.configPath)], { forceNewWindow: true }); }); }); } diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index d598e0b465c..27cdd158e37 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -10,19 +10,23 @@ import * as nls from 'vs/nls'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { dirname } from 'vs/base/common/paths'; -import { IQuickOpenService, IFilePickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { FileKind, isParent } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { isLinux } from 'vs/base/common/platform'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IQuickInputService, IPickOptions, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { getIconClasses } from 'vs/workbench/browser/labels'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { Schemas } from 'vs/base/common/network'; export const ADD_ROOT_FOLDER_COMMAND_ID = 'addRootFolder'; export const ADD_ROOT_FOLDER_LABEL = nls.localize('addFolderToWorkspace', "Add Folder to Workspace..."); @@ -30,26 +34,27 @@ export const ADD_ROOT_FOLDER_LABEL = nls.localize('addFolderToWorkspace', "Add F export const PICK_WORKSPACE_FOLDER_COMMAND_ID = '_workbench.pickWorkspaceFolder'; function pickFolders(buttonLabel: string, title: string, windowService: IWindowService, contextService: IWorkspaceContextService, historyService: IHistoryService): TPromise { + const defaultPathURI = defaultFolderPath(contextService, historyService, Schemas.file); return windowService.showOpenDialog({ buttonLabel, title, properties: ['multiSelections', 'openDirectory', 'createDirectory'], - defaultPath: defaultFolderPath(contextService, historyService) + defaultPath: defaultPathURI && defaultPathURI.fsPath }); } -export function defaultFolderPath(contextService: IWorkspaceContextService, historyService: IHistoryService): string { +export function defaultFolderPath(contextService: IWorkspaceContextService, historyService: IHistoryService, schemeFilter: string): URI { let candidate: URI; // Check for last active file root first... - candidate = historyService.getLastActiveWorkspaceRoot('file'); + candidate = historyService.getLastActiveWorkspaceRoot(schemeFilter); // ...then for last active file if (!candidate) { - candidate = historyService.getLastActiveFile(); + candidate = historyService.getLastActiveFile(schemeFilter); } - return candidate ? dirname(candidate.fsPath) : void 0; + return candidate ? resources.dirname(candidate) : void 0; } @@ -62,29 +67,29 @@ function services(accessor: ServicesAccessor): { windowService: IWindowService, }; } -export function defaultFilePath(contextService: IWorkspaceContextService, historyService: IHistoryService): string { +export function defaultFilePath(contextService: IWorkspaceContextService, historyService: IHistoryService, schemeFilter: string): URI { let candidate: URI; // Check for last active file first... - candidate = historyService.getLastActiveFile(); + candidate = historyService.getLastActiveFile(schemeFilter); // ...then for last active file root if (!candidate) { - candidate = historyService.getLastActiveWorkspaceRoot('file'); + candidate = historyService.getLastActiveWorkspaceRoot(schemeFilter); } - return candidate ? dirname(candidate.fsPath) : void 0; + return candidate ? resources.dirname(candidate) : void 0; } -export function defaultWorkspacePath(contextService: IWorkspaceContextService, historyService: IHistoryService, environmentService: IEnvironmentService): string { +export function defaultWorkspacePath(contextService: IWorkspaceContextService, historyService: IHistoryService, environmentService: IEnvironmentService, schemeFilter: string): URI { // Check for current workspace config file first... - if (contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && !isUntitledWorkspace(contextService.getWorkspace().configuration.fsPath, environmentService)) { - return dirname(contextService.getWorkspace().configuration.fsPath); + if (schemeFilter === Schemas.file && contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && !isUntitledWorkspace(contextService.getWorkspace().configuration.fsPath, environmentService)) { + return resources.dirname(contextService.getWorkspace().configuration); } // ...then fallback to default folder path - return defaultFolderPath(contextService, historyService); + return defaultFolderPath(contextService, historyService, schemeFilter); } function isUntitledWorkspace(path: string, environmentService: IEnvironmentService): boolean { @@ -97,8 +102,8 @@ CommandsRegistry.registerCommand({ id: 'workbench.action.files.openFileFolderInNewWindow', handler: (accessor: ServicesAccessor) => { const { windowService, historyService, contextService } = services(accessor); - - windowService.pickFileFolderAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultFilePath(contextService, historyService) } }); + const defaultPathURI = defaultFilePath(contextService, historyService, Schemas.file); + windowService.pickFileFolderAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } }); @@ -106,8 +111,8 @@ CommandsRegistry.registerCommand({ id: '_files.pickFolderAndOpen', handler: (accessor: ServicesAccessor, forceNewWindow: boolean) => { const { windowService, historyService, contextService } = services(accessor); - - windowService.pickFolderAndOpen({ forceNewWindow, dialogOptions: { defaultPath: defaultFolderPath(contextService, historyService) } }); + const defaultPathURI = defaultFolderPath(contextService, historyService, Schemas.file); + windowService.pickFolderAndOpen({ forceNewWindow, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } }); @@ -115,8 +120,8 @@ CommandsRegistry.registerCommand({ id: 'workbench.action.files.openFolderInNewWindow', handler: (accessor: ServicesAccessor) => { const { windowService, historyService, contextService } = services(accessor); - - windowService.pickFolderAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultFolderPath(contextService, historyService) } }); + const defaultPathURI = defaultFolderPath(contextService, historyService, Schemas.file); + windowService.pickFolderAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } }); @@ -124,8 +129,8 @@ CommandsRegistry.registerCommand({ id: 'workbench.action.files.openFileInNewWindow', handler: (accessor: ServicesAccessor) => { const { windowService, historyService, contextService } = services(accessor); - - windowService.pickFileAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultFilePath(contextService, historyService) } }); + const defaultPathURI = defaultFilePath(contextService, historyService, Schemas.file); + windowService.pickFileAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } }); @@ -133,8 +138,8 @@ CommandsRegistry.registerCommand({ id: 'workbench.action.openWorkspaceInNewWindow', handler: (accessor: ServicesAccessor) => { const { windowService, historyService, contextService, environmentService } = services(accessor); - - windowService.pickWorkspaceAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultWorkspacePath(contextService, historyService, environmentService) } }); + const defaultPathURI = defaultWorkspacePath(contextService, historyService, environmentService, Schemas.file); + windowService.pickWorkspaceAndOpen({ forceNewWindow: true, dialogOptions: { defaultPath: defaultPathURI && defaultPathURI.fsPath } }); } }); @@ -157,10 +162,12 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (accessor, args?: [IPickOptions, CancellationToken]) { +CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (accessor, args?: [IPickOptions, CancellationToken]) { + const quickInputService = accessor.get(IQuickInputService); + const labelService = accessor.get(ILabelService); const contextService = accessor.get(IWorkspaceContextService); - const quickOpenService = accessor.get(IQuickOpenService); - const environmentService = accessor.get(IEnvironmentService); + const modelService = accessor.get(IModelService); + const modeService = accessor.get(IModeService); const folders = contextService.getWorkspace().folders; if (!folders.length) { @@ -170,14 +177,13 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc const folderPicks = folders.map(folder => { return { label: folder.name, - description: getPathLabel(resources.dirname(folder.uri), void 0, environmentService), + description: labelService.getUriLabel(resources.dirname(folder.uri), true), folder, - resource: folder.uri, - fileKind: FileKind.ROOT_FOLDER - } as IFilePickOpenEntry; + iconClasses: getIconClasses(modelService, modeService, folder.uri, FileKind.ROOT_FOLDER) + } as IQuickPickItem; }); - let options: IPickOptions; + let options: IPickOptions; if (args) { options = args[0]; } @@ -186,8 +192,8 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc options = Object.create(null); } - if (!options.autoFocus) { - options.autoFocus = { autoFocusFirstEntry: true }; + if (!options.activeItem) { + options.activeItem = folderPicks[0]; } if (!options.placeHolder) { @@ -207,7 +213,7 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc token = CancellationToken.None; } - return quickOpenService.pick(folderPicks, options, token).then(pick => { + return quickInputService.pick(folderPicks, options, token).then(pick => { if (!pick) { return void 0; } diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 006fcc3c3a5..1287623d743 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -12,8 +12,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IFocusTracker, trackFocus, Dimension } from 'vs/base/browser/dom'; +import { trackFocus, Dimension } from 'vs/base/browser/dom'; /** * Composites are layed out in the sidebar and panel part of the workbench. At a time only one composite @@ -26,17 +25,27 @@ import { IFocusTracker, trackFocus, Dimension } from 'vs/base/browser/dom'; * layout and focus call, but only one create and dispose call. */ export abstract class Composite extends Component implements IComposite { - private readonly _onTitleAreaUpdate: Emitter; - private readonly _onDidFocus: Emitter; - private _focusTracker?: IFocusTracker; - private _focusListenerDisposable?: IDisposable; + private readonly _onTitleAreaUpdate: Emitter = this._register(new Emitter()); + get onTitleAreaUpdate(): Event { return this._onTitleAreaUpdate.event; } + + private _onDidFocus: Emitter; + get onDidFocus(): Event { + if (!this._onDidFocus) { + this._onDidFocus = this._register(new Emitter()); + + const focusTracker = this._register(trackFocus(this.getContainer())); + this._register(focusTracker.onDidFocus(() => this._onDidFocus.fire())); + } + + return this._onDidFocus.event; + } + + protected actionRunner: IActionRunner; private visible: boolean; private parent: HTMLElement; - protected actionRunner: IActionRunner; - /** * Create a new composite with the given ID and context. */ @@ -48,11 +57,9 @@ export abstract class Composite extends Component implements IComposite { super(id, themeService); this.visible = false; - this._onTitleAreaUpdate = new Emitter(); - this._onDidFocus = new Emitter(); } - public getTitle(): string { + getTitle(): string { return null; } @@ -60,44 +67,32 @@ export abstract class Composite extends Component implements IComposite { return this._telemetryService; } - public get onTitleAreaUpdate(): Event { - return this._onTitleAreaUpdate.event; - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. * - * Called to create this composite on the provided builder. This method is only + * Called to create this composite on the provided parent. This method is only * called once during the lifetime of the workbench. * Note that DOM-dependent calculations should be performed from the setVisible() * call. Only then the composite will be part of the DOM. */ - public create(parent: HTMLElement): TPromise { + create(parent: HTMLElement): TPromise { this.parent = parent; return TPromise.as(null); } - public updateStyles(): void { + updateStyles(): void { super.updateStyles(); } /** * Returns the container this composite is being build in. */ - public getContainer(): HTMLElement { + getContainer(): HTMLElement { return this.parent; } - public get onDidFocus(): Event { - this._focusTracker = trackFocus(this.getContainer()); - this._focusListenerDisposable = this._focusTracker.onDidFocus(() => { - this._onDidFocus.fire(); - }); - return this._onDidFocus.event; - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. @@ -110,7 +105,7 @@ export abstract class Composite extends Component implements IComposite { * to do a long running operation from this call. Typically this operation should be * fast though because setVisible might be called many times during a session. */ - public setVisible(visible: boolean): TPromise { + setVisible(visible: boolean): TPromise { this.visible = visible; return TPromise.as(null); @@ -119,19 +114,19 @@ export abstract class Composite extends Component implements IComposite { /** * Called when this composite should receive keyboard focus. */ - public focus(): void { + focus(): void { // Subclasses can implement } /** * Layout the contents of this composite using the provided dimensions. */ - public abstract layout(dimension: Dimension): void; + abstract layout(dimension: Dimension): void; /** * Returns an array of actions to show in the action bar of the composite. */ - public getActions(): IAction[] { + getActions(): IAction[] { return []; } @@ -139,14 +134,14 @@ export abstract class Composite extends Component implements IComposite { * Returns an array of actions to show in the action bar of the composite * in a less prominent way then action from getActions. */ - public getSecondaryActions(): IAction[] { + getSecondaryActions(): IAction[] { return []; } /** * Returns an array of actions to show in the context menu of the composite */ - public getContextMenuActions(): IAction[] { + getContextMenuActions(): IAction[] { return []; } @@ -156,7 +151,7 @@ export abstract class Composite extends Component implements IComposite { * of an action. Returns null to indicate that the action is not rendered through * an action item. */ - public getActionItem(action: IAction): IActionItem { + getActionItem(action: IAction): IActionItem { return null; } @@ -164,7 +159,7 @@ export abstract class Composite extends Component implements IComposite { * Returns the instance of IActionRunner to use with this composite for the * composite tool bar. */ - public getActionRunner(): IActionRunner { + getActionRunner(): IActionRunner { if (!this.actionRunner) { this.actionRunner = new ActionRunner(); } @@ -185,43 +180,28 @@ export abstract class Composite extends Component implements IComposite { /** * Returns true if this composite is currently visible and false otherwise. */ - public isVisible(): boolean { + isVisible(): boolean { return this.visible; } /** * Returns the underlying composite control or null if it is not accessible. */ - public getControl(): ICompositeControl { + getControl(): ICompositeControl { return null; } - - public dispose(): void { - this._onTitleAreaUpdate.dispose(); - this._onDidFocus.dispose(); - - if (this._focusTracker) { - this._focusTracker.dispose(); - } - - if (this._focusListenerDisposable) { - this._focusListenerDisposable.dispose(); - } - - super.dispose(); - } } /** * A composite descriptor is a leightweight descriptor of a composite in the workbench. */ export abstract class CompositeDescriptor { - public id: string; - public name: string; - public cssClass: string; - public order: number; - public keybindingId: string; - public enabled: boolean; + id: string; + name: string; + cssClass: string; + order: number; + keybindingId: string; + enabled: boolean; private ctor: IConstructorSignature0; @@ -235,7 +215,7 @@ export abstract class CompositeDescriptor { this.keybindingId = keybindingId; } - public instantiate(instantiationService: IInstantiationService): T { + instantiate(instantiationService: IInstantiationService): T { return instantiationService.createInstance(this.ctor); } } @@ -243,13 +223,9 @@ export abstract class CompositeDescriptor { export abstract class CompositeRegistry { private readonly _onDidRegister: Emitter> = new Emitter>(); - readonly onDidRegister: Event> = this._onDidRegister.event; + get onDidRegister(): Event> { return this._onDidRegister.event; } - private composites: CompositeDescriptor[]; - - constructor() { - this.composites = []; - } + private composites: CompositeDescriptor[] = []; protected registerComposite(descriptor: CompositeDescriptor): void { if (this.compositeById(descriptor.id) !== null) { @@ -260,7 +236,7 @@ export abstract class CompositeRegistry { this._onDidRegister.fire(descriptor); } - public getComposite(id: string): CompositeDescriptor { + getComposite(id: string): CompositeDescriptor { return this.compositeById(id); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index aff0742f1ce..94b601a5154 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -6,23 +6,22 @@ 'use strict'; import { WORKSPACE_EXTENSION, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { extname, basename } from 'vs/base/common/paths'; +import { extname, basename, normalize } from 'vs/base/common/paths'; import { IFileService } from 'vs/platform/files/common/files'; import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { TPromise } from 'vs/base/common/winjs.base'; import { Schemas } from 'vs/base/common/network'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { DataTransfers } from 'vs/base/browser/dnd'; import { DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults'; import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { getPathLabel } from 'vs/base/common/labels'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; import { MIME_BINARY } from 'vs/base/common/mime'; import { ITree, IDragAndDropData } from 'vs/base/parts/tree/browser/tree'; import { isWindows } from 'vs/base/common/platform'; @@ -176,7 +175,7 @@ export class ResourcesDropHandler { } // Make the window active to handle the drop properly within - return this.windowService.focusWindow().then(() => { + this.windowService.focusWindow().then(() => { // Check for special things being dropped return this.doHandleDrop(untitledOrFileResources).then(isWorkspaceOpening => { @@ -187,7 +186,7 @@ export class ResourcesDropHandler { // Add external ones to recently open list unless dropped resource is a workspace const filesToAddToHistory = untitledOrFileResources.filter(d => d.isExternal && d.resource.scheme === Schemas.file).map(d => d.resource); if (filesToAddToHistory.length) { - this.windowsService.addRecentlyOpened(filesToAddToHistory.map(resource => resource.fsPath)); + this.windowsService.addRecentlyOpened(filesToAddToHistory); } const editors: IResourceEditor[] = untitledOrFileResources.map(untitledOrFileResource => ({ @@ -207,7 +206,7 @@ export class ResourcesDropHandler { afterDrop(targetGroup); }); }); - }).done(null, onUnexpectedError); + }); } private doHandleDrop(untitledOrFileResources: (IDraggedResource | IDraggedEditor)[]): TPromise { @@ -290,16 +289,16 @@ export class ResourcesDropHandler { // Pass focus to window this.windowService.focusWindow(); - let workspacesToOpen: TPromise; + let workspacesToOpen: TPromise; // Open in separate windows if we drop workspaces or just one folder if (workspaces.length > 0 || folders.length === 1) { - workspacesToOpen = TPromise.as([...workspaces, ...folders].map(resources => resources.fsPath)); + workspacesToOpen = TPromise.as([...workspaces, ...folders].map(resources => resources)); } // 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 => [workspace.configPath]); + workspacesToOpen = this.workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [URI.file(workspace.configPath)]); } // Open @@ -370,7 +369,7 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: // Text: allows to paste into text-capable areas const lineDelimiter = isWindows ? '\r\n' : '\n'; - event.dataTransfer.setData(DataTransfers.TEXT, sources.map(source => source.resource.scheme === Schemas.file ? getPathLabel(source.resource) : source.resource.toString()).join(lineDelimiter)); + event.dataTransfer.setData(DataTransfers.TEXT, sources.map(source => source.resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(source.resource.fsPath), true) : source.resource.toString()).join(lineDelimiter)); // Download URL: enables support to drag a tab as file to desktop (only single file supported) if (firstSource.resource.scheme === Schemas.file) { @@ -517,4 +516,4 @@ export class DragAndDropObserver extends Disposable { this.callbacks.onDrop(e); })); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 525a263c59f..0b3c009786a 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -66,19 +66,19 @@ export class EditorDescriptor implements IEditorDescriptor { this.name = name; } - public instantiate(instantiationService: IInstantiationService): BaseEditor { + instantiate(instantiationService: IInstantiationService): BaseEditor { return instantiationService.createInstance(this.ctor); } - public getId(): string { + getId(): string { return this.id; } - public getName(): string { + getName(): string { return this.name; } - public describes(obj: any): boolean { + describes(obj: any): boolean { return obj instanceof BaseEditor && (obj).getId() === this.id; } } @@ -86,15 +86,11 @@ export class EditorDescriptor implements IEditorDescriptor { const INPUT_DESCRIPTORS_PROPERTY = '__$inputDescriptors'; class EditorRegistry implements IEditorRegistry { - private editors: EditorDescriptor[]; + private editors: EditorDescriptor[] = []; - constructor() { - this.editors = []; - } - - public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor): void; - public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor[]): void; - public registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: any): void { + registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor): void; + registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor[]): void; + registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: any): void { // Support both non-array and array parameter let inputDescriptors: SyncDescriptor[] = []; @@ -109,7 +105,7 @@ class EditorRegistry implements IEditorRegistry { this.editors.push(descriptor); } - public getEditor(input: EditorInput): EditorDescriptor { + getEditor(input: EditorInput): EditorDescriptor { const findEditorDescriptors = (input: EditorInput, byInstanceOf?: boolean): EditorDescriptor[] => { const matchingDescriptors: EditorDescriptor[] = []; @@ -161,7 +157,7 @@ class EditorRegistry implements IEditorRegistry { return null; } - public getEditorById(editorId: string): EditorDescriptor { + getEditorById(editorId: string): EditorDescriptor { for (let i = 0; i < this.editors.length; i++) { const editor = this.editors[i]; if (editor.getId() === editorId) { @@ -172,15 +168,15 @@ class EditorRegistry implements IEditorRegistry { return null; } - public getEditors(): EditorDescriptor[] { + getEditors(): EditorDescriptor[] { return this.editors.slice(0); } - public setEditors(editorsToSet: EditorDescriptor[]): void { + setEditors(editorsToSet: EditorDescriptor[]): void { this.editors = editorsToSet; } - public getEditorInputs(): any[] { + getEditorInputs(): any[] { const inputClasses: any[] = []; for (let i = 0; i < this.editors.length; i++) { const editor = this.editors[i]; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 2564f98ca85..e8ecc74e37c 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -5,19 +5,16 @@ 'use strict'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IconLabel, IIconLabelValueOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; import { toResource, IEditorInput } from 'vs/workbench/common/editor'; -import { getPathLabel, IWorkspaceFolderProvider } from 'vs/base/common/labels'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { Schemas } from 'vs/base/common/network'; @@ -26,6 +23,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { DataUri } from 'vs/workbench/common/resources'; +import { ILabelService } from 'vs/platform/label/common/label'; export interface IResourceLabel { name: string; @@ -40,51 +38,47 @@ export interface IResourceLabelOptions extends IIconLabelValueOptions { export class ResourceLabel extends IconLabel { - private toDispose: IDisposable[]; + private _onDidRender = this._register(new Emitter()); + get onDidRender(): Event { return this._onDidRender.event; } + private label: IResourceLabel; private options: IResourceLabelOptions; private computedIconClasses: string[]; private lastKnownConfiguredLangId: string; private computedPathLabel: string; - private _onDidRender = new Emitter(); - readonly onDidRender: Event = this._onDidRender.event; - constructor( container: HTMLElement, options: IIconLabelCreationOptions, @IExtensionService private extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService, @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, - @IEnvironmentService protected environmentService: IEnvironmentService, @IDecorationsService protected decorationsService: IDecorationsService, - @IThemeService private themeService: IThemeService + @IThemeService private themeService: IThemeService, + @ILabelService protected labelService: ILabelService ) { super(container, options); - this.toDispose = []; - this.registerListeners(); } private registerListeners(): void { // update when extensions are registered with potentially new languages - this.toDispose.push(this.extensionService.onDidRegisterExtensions(() => this.render(true /* clear cache */))); + this._register(this.extensionService.onDidRegisterExtensions(() => this.render(true /* clear cache */))); // react to model mode changes - this.toDispose.push(this.modelService.onModelModeChanged(e => this.onModelModeChanged(e))); + this._register(this.modelService.onModelModeChanged(e => this.onModelModeChanged(e))); // react to file decoration changes - this.toDispose.push(this.decorationsService.onDidChangeDecorations(this.onFileDecorationsChanges, this)); + this._register(this.decorationsService.onDidChangeDecorations(this.onFileDecorationsChanges, this)); // react to theme changes - this.toDispose.push(this.themeService.onThemeChange(() => this.render(false))); + this._register(this.themeService.onThemeChange(() => this.render(false))); // react to files.associations changes - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(FILES_ASSOCIATIONS_CONFIG)) { this.render(true /* clear cache */); } @@ -121,7 +115,7 @@ export class ResourceLabel extends IconLabel { } } - public setLabel(label: IResourceLabel, options?: IResourceLabelOptions): void { + setLabel(label: IResourceLabel, options?: IResourceLabelOptions): void { const hasResourceChanged = this.hasResourceChanged(label, options); this.label = label; @@ -145,6 +139,10 @@ export class ResourceLabel extends IconLabel { return true; // same resource but different kind (file, folder) } + if (newResource && this.computedPathLabel !== this.labelService.getUriLabel(newResource)) { + return true; + } + if (newResource && oldResource) { return newResource.toString() !== oldResource.toString(); } @@ -156,7 +154,7 @@ export class ResourceLabel extends IconLabel { return true; } - public clear(): void { + clear(): void { this.label = void 0; this.options = void 0; this.lastKnownConfiguredLangId = void 0; @@ -187,6 +185,7 @@ export class ResourceLabel extends IconLabel { title: '', italic: this.options && this.options.italic, matches: this.options && this.options.matches, + extraClasses: [] }; const resource = this.label.resource; @@ -196,17 +195,18 @@ export class ResourceLabel extends IconLabel { iconLabelOptions.title = this.options.title; } else if (resource && resource.scheme !== Schemas.data /* do not accidentally inline Data URIs */) { if (!this.computedPathLabel) { - this.computedPathLabel = getPathLabel(resource, void 0, this.environmentService); + this.computedPathLabel = this.labelService.getUriLabel(resource); } iconLabelOptions.title = this.computedPathLabel; } - if (!this.computedIconClasses) { - this.computedIconClasses = getIconClasses(this.modelService, this.modeService, resource, this.options && this.options.fileKind); + if (this.options && !this.options.hideIcon) { + if (!this.computedIconClasses) { + this.computedIconClasses = getIconClasses(this.modelService, this.modeService, resource, this.options && this.options.fileKind); + } + iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); } - - iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); if (this.options && this.options.extraClasses) { iconLabelOptions.extraClasses.push(...this.options.extraClasses); } @@ -238,10 +238,9 @@ export class ResourceLabel extends IconLabel { this._onDidRender.fire(); } - public dispose(): void { + dispose(): void { super.dispose(); - this.toDispose = dispose(this.toDispose); this.label = void 0; this.options = void 0; this.lastKnownConfiguredLangId = void 0; @@ -252,7 +251,7 @@ export class ResourceLabel extends IconLabel { export class EditorLabel extends ResourceLabel { - public setEditor(editor: IEditorInput, options?: IResourceLabelOptions): void { + setEditor(editor: IEditorInput, options?: IResourceLabelOptions): void { this.setLabel({ resource: toResource(editor, { supportSideBySide: true }), name: editor.getName(), @@ -264,7 +263,6 @@ export class EditorLabel extends ResourceLabel { export interface IFileLabelOptions extends IResourceLabelOptions { hideLabel?: boolean; hidePath?: boolean; - root?: uri; } export class FileLabel extends ResourceLabel { @@ -273,19 +271,19 @@ export class FileLabel extends ResourceLabel { container: HTMLElement, options: IIconLabelCreationOptions, @IExtensionService extensionService: IExtensionService, - @IWorkspaceContextService contextService: IWorkspaceContextService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService configurationService: IConfigurationService, @IModeService modeService: IModeService, @IModelService modelService: IModelService, - @IEnvironmentService environmentService: IEnvironmentService, @IDecorationsService decorationsService: IDecorationsService, @IThemeService themeService: IThemeService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, + @ILabelService labelService: ILabelService ) { - super(container, options, extensionService, contextService, configurationService, modeService, modelService, environmentService, decorationsService, themeService); + super(container, options, extensionService, configurationService, modeService, modelService, decorationsService, themeService, labelService); } - public setFile(resource: uri, options?: IFileLabelOptions): void { + setFile(resource: uri, options?: IFileLabelOptions): void { const hideLabel = options && options.hideLabel; let name: string; if (!hideLabel) { @@ -304,17 +302,7 @@ export class FileLabel extends ResourceLabel { let description: string; const hidePath = (options && options.hidePath) || (resource.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(resource)); if (!hidePath) { - let rootProvider: IWorkspaceFolderProvider; - if (options && options.root) { - rootProvider = { - getWorkspaceFolder(): { uri } { return { uri: options.root }; }, - getWorkspace(): { folders: { uri: uri }[]; } { return { folders: [{ uri: options.root }] }; }, - }; - } else { - rootProvider = this.contextService; - } - - description = getPathLabel(resources.dirname(resource), rootProvider, this.environmentService); + description = this.labelService.getUriLabel(resources.dirname(resource), true); } this.setLabel({ resource, name, description }, options); @@ -345,15 +333,17 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe // Files else { - // Name - classes.push(`${name}-name-file-icon`); + // Name & Extension(s) + if (name) { + classes.push(`${name}-name-file-icon`); - // Extension(s) - const dotSegments = name.split('.'); - for (let i = 1; i < dotSegments.length; i++) { - classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one + const dotSegments = name.split('.'); + for (let i = 1; i < dotSegments.length; i++) { + classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one + } + + classes.push(`ext-file-icon`); // extra segment to increase file-ext score } - classes.push(`ext-file-icon`); // extra segment to increase file-ext score // Configured Language let configuredLangId = getConfiguredLangId(modelService, resource); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index d28eade1226..750950e9534 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,6 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as errors from 'vs/base/common/errors'; import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; import { QuickInputService } from 'vs/workbench/browser/parts/quickinput/quickInput'; import { Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation } from 'vs/base/browser/ui/sash/sash'; @@ -14,8 +13,8 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { Disposable } from 'vs/base/common/lifecycle'; -import { getZoomFactor } from 'vs/base/browser/browser'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { isMacintosh } from 'vs/base/common/platform'; import { memoize } from 'vs/base/common/decorators'; import { NotificationsCenter } from 'vs/workbench/browser/parts/notifications/notificationsCenter'; import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; @@ -27,8 +26,9 @@ import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activity import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { StatusbarPart } from 'vs/workbench/browser/parts/statusbar/statusbarPart'; +import { getZoomFactor } from 'vs/base/browser/browser'; -const TITLE_BAR_HEIGHT = 22; +const TITLE_BAR_HEIGHT = isMacintosh ? 22 : 30; const STATUS_BAR_HEIGHT = 22; const ACTIVITY_BAR_WIDTH = 50; @@ -69,7 +69,6 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr private _panelHeight: number; private _panelWidth: number; - // Take parts as an object bag since instatation service does not have typings for constructors with 9+ arguments constructor( private parent: HTMLElement, private workbenchContainer: HTMLElement, @@ -117,23 +116,23 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr private registerListeners(): void { this._register(this.themeService.onThemeChange(_ => this.layout())); - this._register(this.parts.editor.onDidPreferredSizeChange(() => this.onDidPreferredSizeChange())); + this._register(this.parts.editor.onDidSizeConstraintsChange(() => this.onDidEditorSizeConstraintsChange())); this.registerSashListeners(); } - private onDidPreferredSizeChange(): void { + private onDidEditorSizeConstraintsChange(): void { if (this.workbenchSize && (this.sidebarWidth || this.panelHeight)) { if (this.editorGroupService.count > 1) { - const preferredEditorPartSize = this.parts.editor.preferredSize; + const minimumEditorPartSize = new Dimension(this.parts.editor.minimumWidth, this.parts.editor.minimumHeight); - const sidebarOverflow = this.workbenchSize.width - this.sidebarWidth < preferredEditorPartSize.width; + const sidebarOverflow = this.workbenchSize.width - this.sidebarWidth < minimumEditorPartSize.width; let panelOverflow = false; if (this.partService.getPanelPosition() === Position.RIGHT) { - panelOverflow = this.workbenchSize.width - this.panelWidth - this.sidebarWidth < preferredEditorPartSize.width; + panelOverflow = this.workbenchSize.width - this.panelWidth - this.sidebarWidth < minimumEditorPartSize.width; } else { - panelOverflow = this.workbenchSize.height - this.panelHeight < preferredEditorPartSize.height; + panelOverflow = this.workbenchSize.height - this.panelHeight < minimumEditorPartSize.height; } // Trigger a layout if we detect that either sidebar or panel overflow @@ -191,11 +190,11 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr minSidebarWidth = 0; } - return Math.max(this.partLayoutInfo.panel.minWidth, this.workbenchSize.width - this.parts.editor.preferredSize.width - minSidebarWidth - this.activitybarWidth); + return Math.max(this.partLayoutInfo.panel.minWidth, this.workbenchSize.width - this.parts.editor.minimumWidth - minSidebarWidth - this.activitybarWidth); } private computeMaxPanelHeight(): number { - return Math.max(this.partLayoutInfo.panel.minHeight, this.sidebarHeight /* simplification for: window.height - status.height - title-height */ - this.parts.editor.preferredSize.height); + return Math.max(this.partLayoutInfo.panel.minHeight, this.sidebarHeight /* simplification for: window.height - status.height - title-height */ - this.parts.editor.minimumHeight); } private get sidebarWidth(): number { @@ -208,13 +207,13 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr private set sidebarWidth(value: number) { const panelMinWidth = this.partService.getPanelPosition() === Position.RIGHT && this.partService.isVisible(Parts.PANEL_PART) ? this.partLayoutInfo.panel.minWidth : 0; - const maxSidebarWidth = this.workbenchSize.width - this.activitybarWidth - this.parts.editor.preferredSize.width - panelMinWidth; + const maxSidebarWidth = this.workbenchSize.width - this.activitybarWidth - this.parts.editor.minimumWidth - panelMinWidth; this._sidebarWidth = Math.max(this.partLayoutInfo.sidebar.minWidth, Math.min(maxSidebarWidth, value)); } @memoize - private get partLayoutInfo() { + public get partLayoutInfo() { return { titlebar: { height: TITLE_BAR_HEIGHT @@ -294,7 +293,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } if (doLayout) { - promise.done(() => this.layout({ source: Parts.SIDEBAR_PART }), errors.onUnexpectedError); + promise.then(() => this.layout({ source: Parts.SIDEBAR_PART })); } })); @@ -332,7 +331,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } if (doLayout) { - promise.done(() => this.layout({ source: Parts.PANEL_PART }), errors.onUnexpectedError); + promise.then(() => this.layout({ source: Parts.PANEL_PART })); } })); @@ -370,7 +369,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } if (doLayout) { - promise.done(() => this.layout({ source: Parts.PANEL_PART }), errors.onUnexpectedError); + promise.then(() => this.layout({ source: Parts.PANEL_PART })); } })); @@ -397,7 +396,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr let optimalWidth = activeViewlet && activeViewlet.getOptimalWidth(); this.sidebarWidth = Math.max(optimalWidth, DEFAULT_SIDEBAR_PART_WIDTH); this.storageService.store(WorkbenchLayout.sashXOneWidthSettingsKey, this.sidebarWidth, StorageScope.GLOBAL); - this.partService.setSideBarHidden(false).done(() => this.layout(), errors.onUnexpectedError); + this.partService.setSideBarHidden(false).then(() => this.layout()); })); this._register(this.sashXTwo.onDidReset(() => { @@ -417,6 +416,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr const isSidebarHidden = !this.partService.isVisible(Parts.SIDEBAR_PART); const sidebarPosition = this.partService.getSideBarPosition(); const panelPosition = this.partService.getPanelPosition(); + const menubarVisibility = this.partService.getMenubarVisibility(); // Sidebar if (this.sidebarWidth === -1) { @@ -424,7 +424,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } this.statusbarHeight = isStatusbarHidden ? 0 : this.partLayoutInfo.statusbar.height; - this.titlebarHeight = isTitlebarHidden ? 0 : this.partLayoutInfo.titlebar.height / getZoomFactor(); // adjust for zoom prevention + this.titlebarHeight = isTitlebarHidden ? 0 : this.partLayoutInfo.titlebar.height / (!menubarVisibility || menubarVisibility === 'hidden' ? getZoomFactor() : 1); // adjust for zoom prevention this.sidebarHeight = this.workbenchSize.height - this.statusbarHeight - this.titlebarHeight; let sidebarSize = new Dimension(this.sidebarWidth, this.sidebarHeight); @@ -486,10 +486,10 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr editorSize.width = this.workbenchSize.width - sidebarSize.width - activityBarSize.width - (panelPosition === Position.RIGHT ? panelDimension.width : 0); editorSize.height = sidebarSize.height - (panelPosition === Position.BOTTOM ? panelDimension.height : 0); - // Adjust for Editor Part preferred width - const preferredEditorPartSize = this.parts.editor.preferredSize; - if (editorSize.width < preferredEditorPartSize.width) { - const missingPreferredEditorWidth = preferredEditorPartSize.width - editorSize.width; + // Adjust for Editor Part minimum width + const minimumEditorPartSize = new Dimension(this.parts.editor.minimumWidth, this.parts.editor.minimumHeight); + if (editorSize.width < minimumEditorPartSize.width) { + const missingPreferredEditorWidth = minimumEditorPartSize.width - editorSize.width; let outstandingMissingPreferredEditorWidth = missingPreferredEditorWidth; // Take from Panel if Panel Position on the Right and Visible @@ -512,9 +512,9 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } } - // Adjust for Editor Part preferred height - if (editorSize.height < preferredEditorPartSize.height) { - const missingPreferredEditorHeight = preferredEditorPartSize.height - editorSize.height; + // Adjust for Editor Part minimum height + if (editorSize.height < minimumEditorPartSize.height) { + const missingPreferredEditorHeight = minimumEditorPartSize.height - editorSize.height; let outstandingMissingPreferredEditorHeight = missingPreferredEditorHeight; // Take from Panel if Panel Position on the Bottom and Visible @@ -702,7 +702,6 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr return this.panelMaximized; } - // change part size along the main axis resizePart(part: Parts, sizeChange: number): void { const panelPosition = this.partService.getPanelPosition(); const sizeChangePxWidth = this.workbenchSize.width * (sizeChange / 100); @@ -713,9 +712,8 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr case Parts.SIDEBAR_PART: this.sidebarWidth = this.sidebarWidth + sizeChangePxWidth; // Sidebar can not become smaller than MIN_PART_WIDTH - const preferredEditorPartSize = this.parts.editor.preferredSize; - if (this.workbenchSize.width - this.sidebarWidth < preferredEditorPartSize.width) { - this.sidebarWidth = this.workbenchSize.width - preferredEditorPartSize.width; + if (this.workbenchSize.width - this.sidebarWidth < this.parts.editor.minimumWidth) { + this.sidebarWidth = this.workbenchSize.width - this.parts.editor.minimumWidth; } doLayout = true; diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 94f68267136..fc87a52eaf3 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -30,9 +30,15 @@ padding-left: 12px; } -.monaco-workbench > .part > .title > .title-label span { +.monaco-workbench > .part > .title > .title-label h2 { font-size: 11px; cursor: default; + font-weight: normal; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .monaco-workbench > .part > .title > .title-label a { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index 8b05f420b0b..f046282ca36 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -31,28 +31,28 @@ export class PanelRegistry extends CompositeRegistry { /** * Registers a panel to the platform. */ - public registerPanel(descriptor: PanelDescriptor): void { + registerPanel(descriptor: PanelDescriptor): void { super.registerComposite(descriptor); } /** * Returns an array of registered panels known to the platform. */ - public getPanels(): PanelDescriptor[] { + getPanels(): PanelDescriptor[] { return this.getComposites() as PanelDescriptor[]; } /** * Sets the id of the panel that should open on startup by default. */ - public setDefaultPanelId(id: string): void { + setDefaultPanelId(id: string): void { this.defaultPanelId = id; } /** * Gets the id of the panel that should open on startup by default. */ - public getDefaultPanelId(): string { + getDefaultPanelId(): string { return this.defaultPanelId; } } @@ -76,7 +76,7 @@ export abstract class TogglePanelAction extends Action { this.panelId = panelId; } - public run(): TPromise { + run(): TPromise { if (this.isPanelShowing()) { return this.partService.setPanelHidden(true); diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 8f147ff99bc..a7ea04b455e 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -47,7 +47,7 @@ export abstract class Part extends Component { * * Called to create title and content area of the part. */ - public create(parent: HTMLElement): void { + create(parent: HTMLElement): void { this.parent = parent; this.titleArea = this.createTitleArea(parent); this.contentArea = this.createContentArea(parent); @@ -60,7 +60,7 @@ export abstract class Part extends Component { /** * Returns the overall part container. */ - public getContainer(): HTMLElement { + getContainer(): HTMLElement { return this.parent; } @@ -95,7 +95,7 @@ export abstract class Part extends Component { /** * Layout title and content area in the given dimension. */ - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { return this.partLayout.layout(dimension); } } @@ -106,7 +106,7 @@ export class PartLayout { constructor(container: HTMLElement, private options: IPartOptions, titleArea: HTMLElement, private contentArea: HTMLElement) { } - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { const { width, height } = dimension; // Return the applied sizes to title and content diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 366b81fe440..d13513f70f7 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -21,8 +21,9 @@ import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colo import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActivityAction, ActivityActionItem, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; +import { ActivityAction, ActivityActionItem, ICompositeBarColors, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositeBarActions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { URI } from 'vs/base/common/uri'; export class ViewletActivityAction extends ActivityAction { @@ -39,7 +40,7 @@ export class ViewletActivityAction extends ActivityAction { super(activity); } - public run(event: any): TPromise { + run(event: any): TPromise { if (event instanceof MouseEvent && event.button === 2) { return TPromise.as(false); // do not run on right click } @@ -85,7 +86,7 @@ export class ToggleViewletAction extends Action { super(_viewlet.id, _viewlet.name); } - public run(): TPromise { + run(): TPromise { const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART); const activeViewlet = this.viewletService.getActiveViewlet(); @@ -116,33 +117,34 @@ export class GlobalActivityActionItem extends ActivityActionItem { super(action, { draggable: false, colors, icon: true }, themeService); } - public render(container: HTMLElement): void { + render(container: HTMLElement): void { super.render(container); // Context menus are triggered on mouse down so that an item can be picked // and executed with releasing the mouse over it - this.$container.on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { DOM.EventHelper.stop(e, true); const event = new StandardMouseEvent(e); this.showContextMenu({ x: event.posx, y: event.posy }); - }); + })); - this.$container.on(DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { let event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { DOM.EventHelper.stop(e, true); - this.showContextMenu(this.$container.getHTMLElement()); + this.showContextMenu(this.container); } - }); + })); - this.$container.on(TouchEventType.Tap, (e: GestureEvent) => { + this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { DOM.EventHelper.stop(e, true); const event = new StandardMouseEvent(e); this.showContextMenu({ x: event.posx, y: event.posy }); - }); + })); } private showContextMenu(location: HTMLElement | { x: number, y: number }): void { @@ -158,6 +160,41 @@ export class GlobalActivityActionItem extends ActivityActionItem { } } +export class PlaceHolderViewletActivityAction extends ViewletActivityAction { + + constructor( + id: string, iconUrl: URI, + @IViewletService viewletService: IViewletService, + @IPartService partService: IPartService, + @ITelemetryService telemetryService: ITelemetryService + ) { + super({ id, name: id, cssClass: `extensionViewlet-placeholder-${id.replace(/\./g, '-')}` }, viewletService, partService, telemetryService); + + const iconClass = `.monaco-workbench > .activitybar .monaco-action-bar .action-label.${this.class}`; // Generate Placeholder CSS to show the icon in the activity bar + DOM.createCSSRule(iconClass, `-webkit-mask: url('${iconUrl || ''}') no-repeat 50% 50%`); + this.enabled = false; + } + + setActivity(activity: IActivity): void { + this.activity = activity; + this.enabled = true; + } +} + +export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinnedAction { + + constructor(id: string, compositeBar: ICompositeBar) { + super({ id, name: id, cssClass: void 0 }, compositeBar); + + this.enabled = false; + } + + setActivity(activity: IActivity): void { + this.label = activity.name; + this.enabled = true; + } +} + registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { // Styling with Outline color (e.g. high contrast theme) diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6f53eadda76..36376370888 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -8,36 +8,39 @@ import 'vs/css!./media/activitybarpart'; import * as nls from 'vs/nls'; import { illegalArgument } from 'vs/base/common/errors'; -import { $ } from 'vs/base/browser/builder'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { GlobalActivityExtensions, IGlobalActivityRegistry, IActivity } from 'vs/workbench/common/activity'; +import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity'; import { Registry } from 'vs/platform/registry/common/platform'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionItem, GlobalActivityAction, ViewletActivityAction, ToggleViewletAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionItem, GlobalActivityAction, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; 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 } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } 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 } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar'; -import { ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; -import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; -import { Dimension, createCSSRule } from 'vs/base/browser/dom'; +import { CompositeBar } from 'vs/workbench/browser/parts/compositeBar'; +import { isMacintosh } from 'vs/base/common/platform'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { scheduleAtNextAnimationFrame, Dimension, addClass } from 'vs/base/browser/dom'; +import { Color } from 'vs/base/common/color'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { URI } from 'vs/base/common/uri'; +import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; interface IPlaceholderComposite { id: string; - iconUrl: string; + iconUrl: URI; } export class ActivitybarPart extends Part { + private static readonly ACTION_HEIGHT = 50; private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets'; private static readonly PLACEHOLDER_VIEWLETS = 'workbench.activity.placeholderViewlets'; private static readonly COLORS = { @@ -46,18 +49,15 @@ export class ActivitybarPart extends Part { badgeForeground: ACTIVITY_BAR_BADGE_FOREGROUND, dragAndDropBackground: ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND }; - private static readonly ACTION_HEIGHT = 50; - - public _serviceBrand: any; private dimension: Dimension; private globalActionBar: ActionBar; - private globalActivityIdToActions: { [globalActivityId: string]: GlobalActivityAction; }; + private globalActivityIdToActions: { [globalActivityId: string]: GlobalActivityAction; } = Object.create(null); private placeholderComposites: IPlaceholderComposite[] = []; private compositeBar: CompositeBar; - private compositeActions: { [compositeId: string]: { activityAction: ViewletActivityAction, pinnedAction: ToggleCompositePinnedAction } }; + private compositeActions: { [compositeId: string]: { activityAction: ViewletActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null); constructor( id: string, @@ -65,15 +65,13 @@ export class ActivitybarPart extends Part { @IInstantiationService private instantiationService: IInstantiationService, @IPartService private partService: IPartService, @IThemeService themeService: IThemeService, + @ILifecycleService private lifecycleService: ILifecycleService, @IStorageService private storageService: IStorageService, - @IExtensionService extensionService: IExtensionService, + @IExtensionService private extensionService: IExtensionService ) { super(id, { hasTitle: false }, themeService); - this.globalActivityIdToActions = Object.create(null); - - this.compositeActions = Object.create(null); - this.compositeBar = this.instantiationService.createInstance(CompositeBar, { + this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, { icon: true, storageId: ActivitybarPart.PINNED_VIEWLETS, orientation: ActionsOrientation.VERTICAL, @@ -87,41 +85,47 @@ export class ActivitybarPart extends Part { compositeSize: 50, colors: ActivitybarPart.COLORS, overflowActionSize: ActivitybarPart.ACTION_HEIGHT + })); + + const previousState = this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEWLETS, StorageScope.GLOBAL, '[]'); + this.placeholderComposites = JSON.parse(previousState); + this.placeholderComposites.forEach((s) => { + if (typeof s.iconUrl === 'object') { + s.iconUrl = URI.revive(s.iconUrl); + } else { + s.iconUrl = void 0; + } }); - const previousState = this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEWLETS, StorageScope.GLOBAL, void 0); - this.placeholderComposites = previousState ? JSON.parse(previousState) : this.compositeBar.getCompositesFromStorage().map(id => ({ id, iconUrl: void 0 })); this.registerListeners(); this.updateCompositebar(); - this.updatePlaceholderComposites(); - - extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions()); - } - - private onDidRegisterExtensions(): void { - this.removeNotExistingPlaceholderComposites(); - this.updateCompositebar(); } private registerListeners(): void { - - this.toUnbind.push(this.viewletService.onDidViewletRegister(() => this.updateCompositebar())); + this._register(this.viewletService.onDidViewletRegister(() => this.updateCompositebar())); // Activate viewlet action on opening of a viewlet - this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId()))); + this._register(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId()))); // Deactivate viewlet action on close - this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.compositeBar.deactivateComposite(viewlet.getId()))); - this.toUnbind.push(this.viewletService.onDidViewletEnablementChange(({ id, enabled }) => { + this._register(this.viewletService.onDidViewletClose(viewlet => this.compositeBar.deactivateComposite(viewlet.getId()))); + this._register(this.viewletService.onDidViewletEnablementChange(({ id, enabled }) => { if (enabled) { this.compositeBar.addComposite(this.viewletService.getViewlet(id)); } else { - this.removeComposite(id); + this.removeComposite(id, true); } })); + + this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); } - public showActivity(viewletOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { + private onDidRegisterExtensions(): void { + this.removeNotExistingComposites(); + this.updateCompositebar(); + } + + showActivity(viewletOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { if (this.viewletService.getViewlet(viewletOrActionId)) { return this.compositeBar.showActivity(viewletOrActionId, badge, clazz, priority); } @@ -144,36 +148,62 @@ export class ActivitybarPart extends Part { return toDisposable(() => action.setBadge(undefined)); } - public createContentArea(parent: HTMLElement): HTMLElement { - const $el = $(parent); - const $result = $('.content').appendTo($el); + createContentArea(parent: HTMLElement): HTMLElement { + const content = document.createElement('div'); + addClass(content, 'content'); + parent.appendChild(content); // Top Actionbar with action items for each viewlet action - this.compositeBar.create($result.getHTMLElement()); + this.compositeBar.create(content); // Top Actionbar with action items for each viewlet action - this.createGlobalActivityActionBar($('.global-activity').appendTo($result).getHTMLElement()); + const globalActivities = document.createElement('div'); + addClass(globalActivities, 'global-activity'); + content.appendChild(globalActivities); - return $result.getHTMLElement(); + this.createGlobalActivityActionBar(globalActivities); + + // TODO@Ben: workaround for https://github.com/Microsoft/vscode/issues/45700 + // It looks like there are rendering glitches on macOS with Chrome 61 when + // using --webkit-mask with a background color that is different from the image + // The workaround is to promote the element onto its own drawing layer. We do + // this only after the workbench has loaded because otherwise there is ugly flicker. + if (isMacintosh) { + this.lifecycleService.when(LifecyclePhase.Running).then(() => { + scheduleAtNextAnimationFrame(() => { // another delay... + scheduleAtNextAnimationFrame(() => { // ...to prevent more flickering on startup + registerThemingParticipant((theme, collector) => { + const activityBarForeground = theme.getColor(ACTIVITY_BAR_FOREGROUND); + if (activityBarForeground && !activityBarForeground.equals(Color.white)) { + // only apply this workaround if the color is different from the image one (white) + collector.addRule('.monaco-workbench .activitybar > .content .monaco-action-bar .action-label { will-change: transform; }'); + } + }); + }); + }); + }); + } + + return content; } - public updateStyles(): void { + updateStyles(): void { super.updateStyles(); // Part container - const container = $(this.getContainer()); + const container = this.getContainer(); const background = this.getColor(ACTIVITY_BAR_BACKGROUND); - container.style('background-color', background); + container.style.backgroundColor = background; const borderColor = this.getColor(ACTIVITY_BAR_BORDER) || this.getColor(contrastBorder); const isPositionLeft = this.partService.getSideBarPosition() === SideBarPosition.LEFT; - container.style('box-sizing', borderColor && isPositionLeft ? 'border-box' : null); - container.style('border-right-width', borderColor && isPositionLeft ? '1px' : null); - container.style('border-right-style', borderColor && isPositionLeft ? 'solid' : null); - container.style('border-right-color', isPositionLeft ? borderColor : null); - container.style('border-left-width', borderColor && !isPositionLeft ? '1px' : null); - container.style('border-left-style', borderColor && !isPositionLeft ? 'solid' : null); - container.style('border-left-color', !isPositionLeft ? borderColor : null); + container.style.boxSizing = borderColor && isPositionLeft ? 'border-box' : null; + container.style.borderRightWidth = borderColor && isPositionLeft ? '1px' : null; + container.style.borderRightStyle = borderColor && isPositionLeft ? 'solid' : null; + container.style.borderRightColor = isPositionLeft ? borderColor : null; + container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : null; + container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : null; + container.style.borderLeftColor = !isPositionLeft ? borderColor : null; } private createGlobalActivityActionBar(container: HTMLElement): void { @@ -183,13 +213,12 @@ export class ActivitybarPart extends Part { .map(d => this.instantiationService.createInstance(d)) .map(a => new GlobalActivityAction(a)); - this.globalActionBar = new ActionBar(container, { + this.globalActionBar = this._register(new ActionBar(container, { actionItemProvider: a => this.instantiationService.createInstance(GlobalActivityActionItem, a, ActivitybarPart.COLORS), orientation: ActionsOrientation.VERTICAL, ariaLabel: nls.localize('globalActions', "Global Actions"), animated: false - }); - this.toUnbind.push(this.globalActionBar); + })); actions.forEach(a => { this.globalActivityIdToActions[a.id] = a; @@ -209,12 +238,14 @@ export class ActivitybarPart extends Part { } else { const placeHolderComposite = this.placeholderComposites.filter(c => c.id === compositeId)[0]; compositeActions = { - activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, placeHolderComposite.iconUrl), + activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, placeHolderComposite && placeHolderComposite.iconUrl), pinnedAction: new PlaceHolderToggleCompositePinnedAction(compositeId, this.compositeBar) }; } + this.compositeActions[compositeId] = compositeActions; } + return compositeActions; } @@ -237,26 +268,21 @@ export class ActivitybarPart extends Part { } } - private updatePlaceholderComposites(): void { - const viewlets = this.viewletService.getViewlets(); - for (const { id } of this.placeholderComposites) { + private removeNotExistingComposites(): void { + const viewlets = this.viewletService.getAllViewlets(); + for (const { id } of this.compositeBar.getComposites()) { if (viewlets.every(viewlet => viewlet.id !== id)) { - this.compositeBar.addComposite({ id, name: id, order: void 0 }); + this.removeComposite(id, false); } } } - private removeNotExistingPlaceholderComposites(): void { - const viewlets = this.viewletService.getViewlets(); - for (const { id } of this.placeholderComposites) { - if (viewlets.every(viewlet => viewlet.id !== id)) { - this.removeComposite(id); - } + private removeComposite(compositeId: string, hide: boolean): void { + if (hide) { + this.compositeBar.hideComposite(compositeId); + } else { + this.compositeBar.removeComposite(compositeId); } - } - - private removeComposite(compositeId: string): void { - this.compositeBar.removeComposite(compositeId); const compositeActions = this.compositeActions[compositeId]; if (compositeActions) { compositeActions.activityAction.dispose(); @@ -268,21 +294,18 @@ export class ActivitybarPart extends Part { private enableCompositeActions(viewlet: ViewletDescriptor): void { const { activityAction, pinnedAction } = this.getCompositeActions(viewlet.id); if (activityAction instanceof PlaceHolderViewletActivityAction) { - activityAction.enable(viewlet); + activityAction.setActivity(viewlet); } if (pinnedAction instanceof PlaceHolderToggleCompositePinnedAction) { - pinnedAction.enable(viewlet); + pinnedAction.setActivity(viewlet); } } - public getPinned(): string[] { + getPinned(): string[] { return this.viewletService.getViewlets().map(v => v.id).filter(id => this.compositeBar.isPinned(id)); } - /** - * Layout title, content and status area in the given dimension. - */ - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { if (!this.partService.isVisible(Parts.ACTIVITYBAR_PART)) { return [dimension]; } @@ -302,62 +325,10 @@ export class ActivitybarPart extends Part { return sizes; } - public shutdown(): void { - const state = this.viewletService.getViewlets().map(viewlet => ({ id: viewlet.id, iconUrl: viewlet.iconUrl })); + shutdown(): void { + const state = this.viewletService.getAllViewlets().map(({ id, iconUrl }) => ({ id, iconUrl })); this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEWLETS, JSON.stringify(state), StorageScope.GLOBAL); + super.shutdown(); } - - public dispose(): void { - if (this.compositeBar) { - this.compositeBar.dispose(); - this.compositeBar = null; - } - - if (this.globalActionBar) { - this.globalActionBar.dispose(); - this.globalActionBar = null; - } - - super.dispose(); - } } - -class PlaceHolderViewletActivityAction extends ViewletActivityAction { - - constructor( - id: string, iconUrl: string, - @IViewletService viewletService: IViewletService, - @IPartService partService: IPartService, - @ITelemetryService telemetryService: ITelemetryService - ) { - super({ id, name: id, cssClass: `extensionViewlet-placeholder-${id.replace(/\./g, '-')}` }, viewletService, partService, telemetryService); - // Generate Placeholder CSS to show the icon in the activity bar - const iconClass = `.monaco-workbench > .activitybar .monaco-action-bar .action-label.${this.class}`; - createCSSRule(iconClass, `-webkit-mask: url('${iconUrl || ''}') no-repeat 50% 50%`); - this.enabled = false; - } - - enable(activity: IActivity): void { - this.label = activity.name; - this.class = activity.cssClass; - this.enabled = true; - } - -} - -class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinnedAction { - - constructor( - id: string, compositeBar: ICompositeBar - ) { - super({ id, name: id, cssClass: void 0 }, compositeBar); - this.enabled = false; - } - - enable(activity: IActivity): void { - this.label = activity.name; - this.enabled = true; - } - -} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts new file mode 100644 index 00000000000..343b3c18c4d --- /dev/null +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -0,0 +1,657 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vs/nls'; +import { Action, IAction } from 'vs/base/common/actions'; +import { illegalArgument } from 'vs/base/common/errors'; +import * as arrays from 'vs/base/common/arrays'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IBadge } from 'vs/workbench/services/activity/common/activity'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors, DraggedCompositeIdentifier } from 'vs/workbench/browser/parts/compositeBarActions'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; + +export interface ICompositeBarOptions { + icon: boolean; + storageId: string; + orientation: ActionsOrientation; + colors: ICompositeBarColors; + compositeSize: number; + overflowActionSize: number; + getActivityAction: (compositeId: string) => ActivityAction; + getCompositePinnedAction: (compositeId: string) => Action; + getOnCompositeClickAction: (compositeId: string) => Action; + getContextMenuActions: () => Action[]; + openComposite: (compositeId: string) => TPromise; + getDefaultCompositeId: () => string; + hidePart: () => TPromise; +} + +export class CompositeBar extends Widget implements ICompositeBar { + + private dimension: Dimension; + + private compositeSwitcherBar: ActionBar; + private compositeOverflowAction: CompositeOverflowActivityAction; + private compositeOverflowActionItem: CompositeOverflowActivityActionItem; + + private model: CompositeBarModel; + private visibleComposites: string[]; + private compositeSizeInBar: Map; + + private compositeTransfer: LocalSelectionTransfer; + + constructor( + private options: ICompositeBarOptions, + @IInstantiationService private instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @IContextMenuService private contextMenuService: IContextMenuService + ) { + super(); + + this.model = new CompositeBarModel(options, storageService); + this.visibleComposites = []; + this.compositeSizeInBar = new Map(); + this.compositeTransfer = LocalSelectionTransfer.getInstance(); + } + + getComposites(): ICompositeBarItem[] { + return this.model.items; + } + + create(parent: HTMLElement): HTMLElement { + const actionBarDiv = parent.appendChild($('.composite-bar')); + this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { + actionItemProvider: (action: Action) => { + if (action instanceof CompositeOverflowActivityAction) { + return this.compositeOverflowActionItem; + } + const item = this.model.findItem(action.id); + return item && this.instantiationService.createInstance(CompositeActionItem, action, item.pinnedAction, () => this.getContextMenuActions(), this.options.colors, this.options.icon, this); + }, + orientation: this.options.orientation, + ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), + animated: false, + })); + + // Contextmenu for composites + this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); + + // Allow to drop at the end to move composites to the end + this._register(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); + + const draggedCompositeId = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id; + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + + const targetItem = this.model.visibleItems[this.model.visibleItems.length - 1]; + if (targetItem && targetItem.id !== draggedCompositeId) { + this.move(draggedCompositeId, targetItem.id); + } + } + })); + + return actionBarDiv; + } + + layout(dimension: Dimension): void { + this.dimension = dimension; + if (dimension.height === 0 || dimension.width === 0) { + // Do not layout if not visible. Otherwise the size measurment would be computed wrongly + return; + } + + if (this.compositeSizeInBar.size === 0) { + // Compute size of each composite by getting the size from the css renderer + // Size is later used for overflow computation + this.computeSizes(this.model.visibleItems); + } + + this.updateCompositeSwitcher(); + } + + addComposite({ id, name, order }: { id: string; name: string, order: number }): void { + // Add to the model + if (this.model.add(id, name, order)) { + this.computeSizes([this.model.findItem(id)]); + this.updateCompositeSwitcher(); + } + } + + removeComposite(id: string): void { + + // If it pinned, unpin it first + if (this.isPinned(id)) { + this.unpin(id); + } + + // Remove from the model + if (this.model.remove(id)) { + this.updateCompositeSwitcher(); + } + } + + hideComposite(id: string): void { + if (this.model.hide(id)) { + this.resetActiveComposite(id); + this.updateCompositeSwitcher(); + } + } + + activateComposite(id: string): void { + const previousActiveItem = this.model.activeItem; + if (this.model.activate(id)) { + // Update if current composite is neither visible nor pinned + // or previous active composite is not pinned + if (this.visibleComposites.indexOf(id) === - 1 || !this.model.activeItem.pinned || (previousActiveItem && !previousActiveItem.pinned)) { + this.updateCompositeSwitcher(); + } + } + } + + deactivateComposite(id: string): void { + const previousActiveItem = this.model.activeItem; + if (this.model.deactivate()) { + if (previousActiveItem && !previousActiveItem.pinned) { + this.updateCompositeSwitcher(); + } + } + } + + showActivity(compositeId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { + if (!badge) { + throw illegalArgument('badge'); + } + + if (typeof priority !== 'number') { + priority = 0; + } + + const activity: ICompositeActivity = { badge, clazz, priority }; + this.model.addActivity(compositeId, activity); + return toDisposable(() => this.model.removeActivity(compositeId, activity)); + } + + pin(compositeId: string, open?: boolean): void { + if (this.model.setPinned(compositeId, true)) { + this.updateCompositeSwitcher(); + + if (open) { + this.options.openComposite(compositeId).then(() => this.activateComposite(compositeId)); // Activate after opening + } + } + } + + unpin(compositeId: string): void { + if (this.model.setPinned(compositeId, false)) { + + this.updateCompositeSwitcher(); + + this.resetActiveComposite(compositeId); + } + } + + private resetActiveComposite(compositeId: string) { + const defaultCompositeId = this.options.getDefaultCompositeId(); + + // Case: composite is not the active one or the active one is a different one + // Solv: we do nothing + if (!this.model.activeItem || this.model.activeItem.id !== compositeId) { + return; + } + + // Deactivate itself + this.deactivateComposite(compositeId); + + // Case: composite is not the default composite and default composite is still showing + // Solv: we open the default composite + if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) { + this.options.openComposite(defaultCompositeId); + } + + // Case: we closed the last visible composite + // Solv: we hide the part + else if (this.visibleComposites.length === 1) { + this.options.hidePart(); + } + + // Case: we closed the default composite + // Solv: we open the next visible composite from top + else { + this.options.openComposite(this.visibleComposites.filter(cid => cid !== compositeId)[0]); + } + } + + isPinned(compositeId: string): boolean { + const item = this.model.findItem(compositeId); + return item && item.pinned; + } + + move(compositeId: string, toCompositeId: string): void { + if (this.model.move(compositeId, toCompositeId)) { + // timeout helps to prevent artifacts from showing up + setTimeout(() => this.updateCompositeSwitcher(), 0); + } + } + + getAction(compositeId): ActivityAction { + const item = this.model.findItem(compositeId); + return item && item.activityAction; + } + + private computeSizes(items: ICompositeBarItem[]): void { + const size = this.options.compositeSize; + if (size) { + items.forEach(composite => this.compositeSizeInBar.set(composite.id, size)); + } else { + if (this.dimension && this.dimension.height !== 0 && this.dimension.width !== 0) { + // Compute sizes only if visible. Otherwise the size measurment would be computed wrongly. + const currentItemsLength = this.compositeSwitcherBar.items.length; + this.compositeSwitcherBar.push(items.map(composite => composite.activityAction)); + items.map((composite, index) => this.compositeSizeInBar.set(composite.id, this.options.orientation === ActionsOrientation.VERTICAL + ? this.compositeSwitcherBar.getHeight(currentItemsLength + index) + : this.compositeSwitcherBar.getWidth(currentItemsLength + index) + )); + items.forEach(() => this.compositeSwitcherBar.pull(this.compositeSwitcherBar.items.length - 1)); + } + } + } + + private updateCompositeSwitcher(): void { + if (!this.compositeSwitcherBar || !this.dimension) { + return; // We have not been rendered yet so there is nothing to update. + } + + let compositesToShow = this.model.visibleItems.filter(item => + item.pinned + || (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */ + ).map(item => item.id); + + // Ensure we are not showing more composites than we have height for + let overflows = false; + let maxVisible = compositesToShow.length; + let size = 0; + const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width; + for (let i = 0; i < compositesToShow.length && size <= limit; i++) { + size += this.compositeSizeInBar.get(compositesToShow[i]); + if (size > limit) { + maxVisible = i; + } + } + overflows = compositesToShow.length > maxVisible; + + if (overflows) { + size -= this.compositeSizeInBar.get(compositesToShow[maxVisible]); + compositesToShow = compositesToShow.slice(0, maxVisible); + size += this.options.overflowActionSize; + } + // Check if we need to make extra room for the overflow action + if (size > limit) { + size -= this.compositeSizeInBar.get(compositesToShow.pop()); + } + + // We always try show the active composite + if (this.model.activeItem && compositesToShow.every(compositeId => compositeId !== this.model.activeItem.id)) { + const removedComposite = compositesToShow.pop(); + size = size - this.compositeSizeInBar.get(removedComposite) + this.compositeSizeInBar.get(this.model.activeItem.id); + compositesToShow.push(this.model.activeItem.id); + } + + // The active composite might have bigger size than the removed composite, check for overflow again + if (size > limit) { + compositesToShow.length ? compositesToShow.splice(compositesToShow.length - 2, 1) : compositesToShow.pop(); + } + + const visibleCompositesChange = !arrays.equals(compositesToShow, this.visibleComposites); + + // Pull out overflow action if there is a composite change so that we can add it to the end later + if (this.compositeOverflowAction && visibleCompositesChange) { + this.compositeSwitcherBar.pull(this.compositeSwitcherBar.length() - 1); + + this.compositeOverflowAction.dispose(); + this.compositeOverflowAction = null; + + this.compositeOverflowActionItem.dispose(); + this.compositeOverflowActionItem = null; + } + + // Pull out composites that overflow or got hidden + const compositesToRemove: number[] = []; + this.visibleComposites.forEach((compositeId, index) => { + if (compositesToShow.indexOf(compositeId) === -1) { + compositesToRemove.push(index); + } + }); + compositesToRemove.reverse().forEach(index => { + const actionItem = this.compositeSwitcherBar.items[index]; + this.compositeSwitcherBar.pull(index); + actionItem.dispose(); + this.visibleComposites.splice(index, 1); + }); + + // Update the positions of the composites + compositesToShow.forEach((compositeId, newIndex) => { + const currentIndex = this.visibleComposites.indexOf(compositeId); + if (newIndex !== currentIndex) { + if (currentIndex !== -1) { + const actionItem = this.compositeSwitcherBar.items[currentIndex]; + this.compositeSwitcherBar.pull(currentIndex); + actionItem.dispose(); + this.visibleComposites.splice(currentIndex, 1); + } + + this.compositeSwitcherBar.push(this.model.findItem(compositeId).activityAction, { label: true, icon: this.options.icon, index: newIndex }); + this.visibleComposites.splice(newIndex, 0, compositeId); + } + }); + + // Add overflow action as needed + if ((visibleCompositesChange && overflows) || this.compositeSwitcherBar.length() === 0) { + this.compositeOverflowAction = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => this.compositeOverflowActionItem.showMenu()); + this.compositeOverflowActionItem = this.instantiationService.createInstance( + CompositeOverflowActivityActionItem, + this.compositeOverflowAction, + () => this.getOverflowingComposites(), + () => this.model.activeItem ? this.model.activeItem.id : void 0, + (compositeId: string) => { + const item = this.model.findItem(compositeId); + return item && item.activity[0] && item.activity[0].badge; + }, + this.options.getOnCompositeClickAction, + this.options.colors + ); + + this.compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true }); + } + + // Persist + this.model.saveState(); + } + + private getOverflowingComposites(): { id: string, name: string }[] { + let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id); + + // Show the active composite even if it is not pinned + if (this.model.activeItem && !this.model.activeItem.pinned) { + overflowingIds.push(this.model.activeItem.id); + } + + overflowingIds = overflowingIds.filter(compositeId => this.visibleComposites.indexOf(compositeId) === -1); + return this.model.visibleItems.filter(c => overflowingIds.indexOf(c.id) !== -1); + } + + private showContextMenu(e: MouseEvent): void { + EventHelper.stop(e, true); + const event = new StandardMouseEvent(e); + this.contextMenuService.showContextMenu({ + getAnchor: () => { return { x: event.posx, y: event.posy }; }, + getActions: () => TPromise.as(this.getContextMenuActions()) + }); + } + + private getContextMenuActions(): IAction[] { + const actions: IAction[] = this.model.visibleItems + .map(({ id, name, activityAction }) => ({ + id, + label: name || id, + checked: this.isPinned(id), + enabled: activityAction.enabled, + run: () => { + if (this.isPinned(id)) { + this.unpin(id); + } else { + this.pin(id, true); + } + } + })); + const otherActions = this.options.getContextMenuActions(); + if (otherActions.length) { + actions.push(new Separator()); + actions.push(...otherActions); + } + return actions; + } +} + +interface ISerializedCompositeBarItem { + id: string; + pinned: boolean; + order: number; + visible: boolean; +} + +interface ICompositeBarItem extends ISerializedCompositeBarItem { + name: string; + activityAction: ActivityAction; + pinnedAction: Action; + activity: ICompositeActivity[]; +} + +class CompositeBarModel { + + private readonly options: ICompositeBarOptions; + readonly items: ICompositeBarItem[]; + + activeItem: ICompositeBarItem; + + constructor( + options: ICompositeBarOptions, + private storageService: IStorageService, + ) { + this.options = options; + this.items = this.loadItemStates(); + } + + get visibleItems(): ICompositeBarItem[] { + return this.items.filter(item => item.visible); + } + + private createCompositeBarItem(id: string, name: string, order: number, pinned: boolean, visible: boolean): ICompositeBarItem { + const options = this.options; + return { + id, name, pinned, order, visible, + activity: [], + get activityAction() { + return options.getActivityAction(id); + }, + get pinnedAction() { + return options.getCompositePinnedAction(id); + } + }; + } + + add(id: string, name: string, order: number): boolean { + const item = this.findItem(id); + if (item) { + let changed = false; + item.name = name; + if (!isUndefinedOrNull(order)) { + changed = item.order !== order; + item.order = order; + } + if (!item.visible) { + item.visible = true; + changed = true; + } + return changed; + } else { + const item = this.createCompositeBarItem(id, name, order, true, true); + if (isUndefinedOrNull(order)) { + this.items.push(item); + } else { + let index = 0; + while (index < this.items.length && this.items[index].order < order) { + index++; + } + this.items.splice(index, 0, item); + } + return true; + } + } + + remove(id: string): boolean { + for (let index = 0; index < this.items.length; index++) { + if (this.items[index].id === id) { + this.items.splice(index, 1); + return true; + } + } + return false; + } + + hide(id: string): boolean { + for (const item of this.items) { + if (item.id === id) { + if (item.visible) { + item.visible = false; + return true; + } + return false; + } + } + return false; + } + + move(compositeId: string, toCompositeId: string): boolean { + + const fromIndex = this.findIndex(compositeId); + const toIndex = this.findIndex(toCompositeId); + + // Make sure both items are known to the model + if (fromIndex === -1 || toIndex === -1) { + return false; + } + + const sourceItem = this.items.splice(fromIndex, 1)[0]; + this.items.splice(toIndex, 0, sourceItem); + + // Make sure a moved composite gets pinned + sourceItem.pinned = true; + + return true; + } + + setPinned(id: string, pinned: boolean): boolean { + for (let index = 0; index < this.items.length; index++) { + const item = this.items[index]; + if (item.id === id) { + if (item.pinned !== pinned) { + item.pinned = pinned; + return true; + } + return false; + } + } + return false; + } + + addActivity(id: string, activity: ICompositeActivity): boolean { + const item = this.findItem(id); + if (item) { + const stack = item.activity; + for (let i = 0; i <= stack.length; i++) { + if (i === stack.length) { + stack.push(activity); + break; + } else if (stack[i].priority <= activity.priority) { + stack.splice(i, 0, activity); + break; + } + } + this.updateActivity(id); + return true; + } + return false; + } + + removeActivity(id: string, activity: ICompositeActivity): boolean { + const item = this.findItem(id); + if (item) { + const index = item.activity.indexOf(activity); + if (index !== -1) { + item.activity.splice(index, 1); + this.updateActivity(id); + return true; + } + } + return false; + } + + updateActivity(id: string): void { + const item = this.findItem(id); + if (item) { + if (item.activity.length) { + const [{ badge, clazz }] = item.activity; + item.activityAction.setBadge(badge, clazz); + } + else { + item.activityAction.setBadge(undefined); + } + } + } + + activate(id: string): boolean { + if (!this.activeItem || this.activeItem.id !== id) { + if (this.activeItem) { + this.deactivate(); + } + for (let index = 0; index < this.items.length; index++) { + const item = this.items[index]; + if (item.id === id) { + this.activeItem = item; + this.activeItem.activityAction.activate(); + return true; + } + } + } + return false; + } + + deactivate(): boolean { + if (this.activeItem) { + this.activeItem.activityAction.deactivate(); + this.activeItem = void 0; + return true; + } + return false; + } + + findItem(id: string): ICompositeBarItem { + return this.items.filter(item => item.id === id)[0]; + } + + private findIndex(id: string): number { + for (let index = 0; index < this.items.length; index++) { + if (this.items[index].id === id) { + return index; + } + } + return -1; + } + + private loadItemStates(): ICompositeBarItem[] { + const storedStates = >JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, '[]')); + return storedStates.map(c => { + const serialized: ISerializedCompositeBarItem = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: void 0, visible: true } : c; + return this.createCompositeBarItem(serialized.id, void 0, serialized.order, serialized.pinned, isUndefinedOrNull(serialized.visible) ? true : serialized.visible); + }); + } + + saveState(): void { + const serialized = this.items.map(({ id, pinned, order, visible }) => ({ id, pinned, order, visible })); + this.storageService.store(this.options.storageId, JSON.stringify(serialized), StorageScope.GLOBAL); + } +} diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts new file mode 100644 index 00000000000..5937ddcbad7 --- /dev/null +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -0,0 +1,651 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as dom from 'vs/base/browser/dom'; +import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { dispose, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; +import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { DelayedDragHandler } from 'vs/base/browser/dnd'; +import { IActivity } from 'vs/workbench/common/activity'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Event, Emitter } from 'vs/base/common/event'; +import { DragAndDropObserver, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; + +export interface ICompositeActivity { + badge: IBadge; + clazz: string; + priority: number; +} + +export interface ICompositeBar { + /** + * Unpins a composite from the composite bar. + */ + unpin(compositeId: string): void; + + /** + * Pin a composite inside the composite bar. + */ + pin(compositeId: string): void; + + /** + * Find out if a composite is pinned in the composite bar. + */ + isPinned(compositeId: string): boolean; + + /** + * Reorder composite ordering by moving a composite to the location of another composite. + */ + move(compositeId: string, tocompositeId: string): void; +} + +export class ActivityAction extends Action { + + private _onDidChangeActivity = new Emitter(); + get onDidChangeActivity(): Event { return this._onDidChangeActivity.event; } + + private _onDidChangeBadge = new Emitter(); + get onDidChangeBadge(): Event { return this._onDidChangeBadge.event; } + + private badge: IBadge; + private clazz: string | undefined; + + constructor(private _activity: IActivity) { + super(_activity.id, _activity.name, _activity.cssClass); + + this.badge = null; + } + + get activity(): IActivity { + return this._activity; + } + + set activity(activity: IActivity) { + this._activity = activity; + this._onDidChangeActivity.fire(this); + } + + activate(): void { + if (!this.checked) { + this._setChecked(true); + } + } + + deactivate(): void { + if (this.checked) { + this._setChecked(false); + } + } + + getBadge(): IBadge { + return this.badge; + } + + getClass(): string | undefined { + return this.clazz; + } + + setBadge(badge: IBadge, clazz?: string): void { + this.badge = badge; + this.clazz = clazz; + this._onDidChangeBadge.fire(this); + } + + dispose(): void { + this._onDidChangeActivity.dispose(); + this._onDidChangeBadge.dispose(); + + super.dispose(); + } +} + +export interface ICompositeBarColors { + backgroundColor: string; + badgeBackground: string; + badgeForeground: string; + dragAndDropBackground: string; +} + +export interface IActivityActionItemOptions extends IBaseActionItemOptions { + icon?: boolean; + colors: ICompositeBarColors; +} + +export class ActivityActionItem extends BaseActionItem { + protected container: HTMLElement; + protected label: HTMLElement; + protected badge: HTMLElement; + protected options: IActivityActionItemOptions; + + private badgeContent: HTMLElement; + private badgeDisposable: IDisposable = Disposable.None; + private mouseUpTimeout: number; + + constructor( + action: ActivityAction, + options: IActivityActionItemOptions, + @IThemeService protected themeService: IThemeService + ) { + super(null, action, options); + + this._register(this.themeService.onThemeChange(this.onThemeChange, this)); + this._register(action.onDidChangeActivity(this.updateActivity, this)); + this._register(action.onDidChangeBadge(this.updateBadge, this)); + } + + protected get activity(): IActivity { + return (this._action as ActivityAction).activity; + } + + protected updateStyles(): void { + const theme = this.themeService.getTheme(); + + // Label + if (this.label && this.options.icon) { + const background = theme.getColor(this.options.colors.backgroundColor); + + this.label.style.backgroundColor = background ? background.toString() : null; + } + + // Badge + if (this.badgeContent) { + const badgeForeground = theme.getColor(this.options.colors.badgeForeground); + const badgeBackground = theme.getColor(this.options.colors.badgeBackground); + const contrastBorderColor = theme.getColor(contrastBorder); + + this.badgeContent.style.color = badgeForeground ? badgeForeground.toString() : null; + this.badgeContent.style.backgroundColor = badgeBackground ? badgeBackground.toString() : null; + + this.badgeContent.style.borderStyle = contrastBorderColor ? 'solid' : null; + this.badgeContent.style.borderWidth = contrastBorderColor ? '1px' : null; + this.badgeContent.style.borderColor = contrastBorderColor ? contrastBorderColor.toString() : null; + } + } + + render(container: HTMLElement): void { + super.render(container); + + this.container = container; + + // Make the container tab-able for keyboard navigation + this.container.tabIndex = 0; + this.container.setAttribute('role', this.options.icon ? 'button' : 'tab'); + + // Try hard to prevent keyboard only focus feedback when using mouse + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_DOWN, () => { + dom.addClass(this.container, 'clicked'); + })); + + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_UP, () => { + if (this.mouseUpTimeout) { + clearTimeout(this.mouseUpTimeout); + } + + this.mouseUpTimeout = setTimeout(() => { + dom.removeClass(this.container, 'clicked'); + }, 800); // delayed to prevent focus feedback from showing on mouse up + })); + + // Label + this.label = dom.append(this.element, dom.$('a.action-label')); + + // Badge + this.badge = dom.append(this.element, dom.$('.badge')); + this.badgeContent = dom.append(this.badge, dom.$('.badge-content')); + + dom.hide(this.badge); + + this.updateActivity(); + this.updateStyles(); + } + + private onThemeChange(theme: ITheme): void { + this.updateStyles(); + } + + protected updateActivity(): void { + this.updateLabel(); + this.updateTitle(this.activity.name); + this.updateBadge(); + } + + protected updateBadge(): void { + const action = this.getAction(); + if (!this.badge || !this.badgeContent || !(action instanceof ActivityAction)) { + return; + } + + const badge = action.getBadge(); + const clazz = action.getClass(); + + this.badgeDisposable.dispose(); + this.badgeDisposable = Disposable.None; + + dom.clearNode(this.badgeContent); + dom.hide(this.badge); + + if (badge) { + + // Number + if (badge instanceof NumberBadge) { + if (badge.number) { + let number = badge.number.toString(); + if (badge.number > 9999) { + number = nls.localize('largeNumberBadge', '10k+'); + } else if (badge.number > 999) { + number = number.charAt(0) + 'k'; + } + this.badgeContent.textContent = number; + dom.show(this.badge); + } + } + + // Text + else if (badge instanceof TextBadge) { + this.badgeContent.textContent = badge.text; + dom.show(this.badge); + } + + // Text + else if (badge instanceof IconBadge) { + dom.show(this.badge); + } + + // Progress + else if (badge instanceof ProgressBadge) { + dom.show(this.badge); + } + + if (clazz) { + dom.addClasses(this.badge, clazz); + this.badgeDisposable = toDisposable(() => dom.removeClasses(this.badge, clazz)); + } + } + + // Title + let title: string; + if (badge && badge.getDescription()) { + if (this.activity.name) { + title = nls.localize('badgeTitle', "{0} - {1}", this.activity.name, badge.getDescription()); + } else { + title = badge.getDescription(); + } + } else { + title = this.activity.name; + } + this.updateTitle(title); + } + + protected updateLabel(): void { + if (this.activity.cssClass) { + dom.addClasses(this.label, this.activity.cssClass); + } + if (!this.options.icon) { + this.label.textContent = this.getAction().label; + } + } + + private updateTitle(title: string): void { + [this.label, this.badge, this.container].forEach(element => { + if (element) { + element.setAttribute('aria-label', title); + element.title = title; + } + }); + } + + dispose(): void { + super.dispose(); + + if (this.mouseUpTimeout) { + clearTimeout(this.mouseUpTimeout); + } + + this.badge.remove(); + } +} + +export class CompositeOverflowActivityAction extends ActivityAction { + + constructor( + private showMenu: () => void + ) { + super({ + id: 'additionalComposites.action', + name: nls.localize('additionalViews', "Additional Views"), + cssClass: 'toggle-more' + }); + } + + run(event: any): TPromise { + this.showMenu(); + + return TPromise.as(true); + } +} + +export class CompositeOverflowActivityActionItem extends ActivityActionItem { + private actions: Action[]; + + constructor( + action: ActivityAction, + private getOverflowingComposites: () => { id: string, name: string }[], + private getActiveCompositeId: () => string, + private getBadge: (compositeId: string) => IBadge, + private getCompositeOpenAction: (compositeId: string) => Action, + colors: ICompositeBarColors, + @IContextMenuService private contextMenuService: IContextMenuService, + @IThemeService themeService: IThemeService + ) { + super(action, { icon: true, colors }, themeService); + } + + showMenu(): void { + if (this.actions) { + dispose(this.actions); + } + + this.actions = this.getActions(); + + this.contextMenuService.showContextMenu({ + getAnchor: () => this.element, + getActions: () => TPromise.as(this.actions), + onHide: () => dispose(this.actions) + }); + } + + private getActions(): Action[] { + return this.getOverflowingComposites().map(composite => { + const action = this.getCompositeOpenAction(composite.id); + action.radio = this.getActiveCompositeId() === action.id; + + const badge = this.getBadge(composite.id); + let suffix: string | number; + if (badge instanceof NumberBadge) { + suffix = badge.number; + } else if (badge instanceof TextBadge) { + suffix = badge.text; + } + + if (suffix) { + action.label = nls.localize('numberBadge', "{0} ({1})", composite.name, suffix); + } else { + action.label = composite.name; + } + + return action; + }); + } + + dispose(): void { + super.dispose(); + + this.actions = dispose(this.actions); + } +} + +class ManageExtensionAction extends Action { + + constructor( + @ICommandService private commandService: ICommandService + ) { + super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension")); + } + + run(id: string): TPromise { + return this.commandService.executeCommand('_extensions.manage', id); + } +} + +export class DraggedCompositeIdentifier { + constructor(private _compositeId: string) { } + + get id(): string { + return this._compositeId; + } +} + +export class CompositeActionItem extends ActivityActionItem { + + private static manageExtensionAction: ManageExtensionAction; + + private compositeActivity: IActivity; + private cssClass: string; + private compositeTransfer: LocalSelectionTransfer; + + constructor( + private compositeActivityAction: ActivityAction, + private toggleCompositePinnedAction: Action, + private contextMenuActionsProvider: () => Action[], + colors: ICompositeBarColors, + icon: boolean, + private compositeBar: ICompositeBar, + @IContextMenuService private contextMenuService: IContextMenuService, + @IKeybindingService private keybindingService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService + ) { + super(compositeActivityAction, { draggable: true, colors, icon }, themeService); + + this.cssClass = compositeActivityAction.class; + this.compositeTransfer = LocalSelectionTransfer.getInstance(); + + if (!CompositeActionItem.manageExtensionAction) { + CompositeActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); + } + + this._register(compositeActivityAction.onDidChangeActivity(() => { this.compositeActivity = null; this.updateActivity(); }, this)); + } + + protected get activity(): IActivity { + if (!this.compositeActivity) { + let activityName: string; + const keybinding = this.getKeybindingLabel(this.compositeActivityAction.activity.keybindingId); + if (keybinding) { + activityName = nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding); + } else { + activityName = this.compositeActivityAction.activity.name; + } + + this.compositeActivity = { + id: this.compositeActivityAction.activity.id, + cssClass: this.cssClass, + name: activityName + }; + } + + return this.compositeActivity; + } + + private getKeybindingLabel(id: string): string { + const kb = this.keybindingService.lookupKeybinding(id); + if (kb) { + return kb.getLabel(); + } + + return null; + } + + render(container: HTMLElement): void { + super.render(container); + + this.updateChecked(); + this.updateEnabled(); + + this._register(dom.addDisposableListener(this.container, dom.EventType.CONTEXT_MENU, e => { + dom.EventHelper.stop(e, true); + + this.showContextMenu(container); + })); + + // Allow to drag + this._register(dom.addDisposableListener(this.container, dom.EventType.DRAG_START, (e: DragEvent) => { + e.dataTransfer.effectAllowed = 'move'; + + // Registe as dragged to local transfer + this.compositeTransfer.setData([new DraggedCompositeIdentifier(this.activity.id)], DraggedCompositeIdentifier.prototype); + + // Trigger the action even on drag start to prevent clicks from failing that started a drag + if (!this.getAction().checked) { + this.getAction().run(); + } + })); + + this._register(new DragAndDropObserver(this.container, { + onDragEnter: e => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) && this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id !== this.activity.id) { + this.updateFromDragging(container, true); + } + }, + + onDragLeave: e => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + this.updateFromDragging(container, false); + } + }, + + onDragEnd: e => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + this.updateFromDragging(container, false); + + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + } + }, + + onDrop: e => { + dom.EventHelper.stop(e, true); + + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + const draggedCompositeId = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype)[0].id; + if (draggedCompositeId !== this.activity.id) { + this.updateFromDragging(container, false); + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + + this.compositeBar.move(draggedCompositeId, this.activity.id); + } + } + } + })); + + // Activate on drag over to reveal targets + [this.badge, this.label].forEach(b => new DelayedDragHandler(b, () => { + if (!this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) && !this.getAction().checked) { + this.getAction().run(); + } + })); + + this.updateStyles(); + } + + private updateFromDragging(element: HTMLElement, isDragging: boolean): void { + const theme = this.themeService.getTheme(); + const dragBackground = theme.getColor(this.options.colors.dragAndDropBackground); + + element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null; + } + + private showContextMenu(container: HTMLElement): void { + const actions: Action[] = [this.toggleCompositePinnedAction]; + if ((this.compositeActivityAction.activity).extensionId) { + actions.push(new Separator()); + actions.push(CompositeActionItem.manageExtensionAction); + } + + const isPinned = this.compositeBar.isPinned(this.activity.id); + if (isPinned) { + this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide"); + this.toggleCompositePinnedAction.checked = false; + } else { + this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep"); + } + + const otherActions = this.contextMenuActionsProvider(); + if (otherActions.length) { + actions.push(new Separator()); + actions.push(...otherActions); + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => container, + getActionsContext: () => this.activity.id, + getActions: () => TPromise.as(actions) + }); + } + + focus(): void { + this.container.focus(); + } + + protected updateClass(): void { + if (this.cssClass) { + dom.removeClasses(this.label, this.cssClass); + } + + this.cssClass = this.getAction().class; + if (this.cssClass) { + dom.addClasses(this.label, this.cssClass); + } + } + + protected updateChecked(): void { + if (this.getAction().checked) { + dom.addClass(this.container, 'checked'); + this.container.setAttribute('aria-label', nls.localize('compositeActive', "{0} active", this.container.title)); + } else { + dom.removeClass(this.container, 'checked'); + this.container.setAttribute('aria-label', this.container.title); + } + } + + protected updateEnabled(): void { + if (this.getAction().enabled) { + dom.removeClass(this.element, 'disabled'); + } else { + dom.addClass(this.element, 'disabled'); + } + } + + dispose(): void { + super.dispose(); + + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + + this.label.remove(); + } +} + +export class ToggleCompositePinnedAction extends Action { + + constructor( + private activity: IActivity, + private compositeBar: ICompositeBar + ) { + super('show.toggleCompositePinned', activity ? activity.name : nls.localize('toggle', "Toggle View Pinned")); + + this.checked = this.activity && this.compositeBar.isPinned(this.activity.id); + } + + run(context: string): TPromise { + const id = this.activity ? this.activity.id : context; + + if (this.compositeBar.isPinned(id)) { + this.compositeBar.unpin(id); + } else { + this.compositeBar.pin(id); + } + + return TPromise.as(true); + } +} diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 7eba9f60160..ac8e350ea58 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -10,7 +10,6 @@ import * as nls from 'vs/nls'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Builder, $ } from 'vs/base/browser/builder'; import * as strings from 'vs/base/common/strings'; import { Emitter } from 'vs/base/common/event'; import * as types from 'vs/base/common/types'; @@ -35,7 +34,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension } from 'vs/base/browser/dom'; +import { Dimension, append, $, addClass, hide, show, addClasses } from 'vs/base/browser/dom'; export interface ICompositeTitleLabel { @@ -51,21 +50,24 @@ export interface ICompositeTitleLabel { } export abstract class CompositePart extends Part { + + protected _onDidCompositeOpen = this._register(new Emitter()); + protected _onDidCompositeClose = this._register(new Emitter()); + + protected toolBar: ToolBar; + private instantiatedCompositeListeners: IDisposable[]; - private mapCompositeToCompositeContainer: { [compositeId: string]: Builder; }; + private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; }; private mapActionsBindingToComposite: { [compositeId: string]: () => void; }; private mapProgressServiceToComposite: { [compositeId: string]: IProgressService; }; private activeComposite: Composite; private lastActiveCompositeId: string; private instantiatedComposites: Composite[]; private titleLabel: ICompositeTitleLabel; - protected toolBar: ToolBar; private progressBar: ProgressBar; private contentAreaSize: Dimension; private telemetryActionsListener: IDisposable; private currentCompositeOpenToken: string; - protected _onDidCompositeOpen = new Emitter(); - protected _onDidCompositeClose = new Emitter(); constructor( private notificationService: INotificationService, @@ -219,13 +221,12 @@ export abstract class CompositePart extends Part { if (!compositeContainer) { // Build Container off-DOM - compositeContainer = $().div({ - 'class': ['composite', this.compositeCSSClass], - id: composite.getId() - }, div => { - createCompositePromise = composite.create(div.getHTMLElement()).then(() => { - composite.updateStyles(); - }); + compositeContainer = $('.composite'); + addClasses(compositeContainer, this.compositeCSSClass); + compositeContainer.id = composite.getId(); + + createCompositePromise = composite.create(compositeContainer).then(() => { + composite.updateStyles(); }); // Remember composite container @@ -252,8 +253,8 @@ export abstract class CompositePart extends Part { } // Take Composite on-DOM and show - compositeContainer.build(this.getContentArea()); - compositeContainer.show(); + this.getContentArea().appendChild(compositeContainer); + show(compositeContainer); // Setup action runner this.toolBar.actionRunner = composite.getActionRunner(); @@ -386,8 +387,8 @@ export abstract class CompositePart extends Part { return composite.setVisible(false).then(() => { // Take Container Off-DOM and hide - compositeContainer.offDOM(); - compositeContainer.hide(); + compositeContainer.remove(); + hide(compositeContainer); // Clear any running Progress this.progressBar.stop().hide(); @@ -400,48 +401,41 @@ export abstract class CompositePart extends Part { }); } - public createTitleArea(parent: HTMLElement): HTMLElement { + createTitleArea(parent: HTMLElement): HTMLElement { // Title Area Container - const titleArea = $(parent).div({ - 'class': ['composite', 'title'] - }); + const titleArea = append(parent, $('.composite')); + addClass(titleArea, 'title'); // Left Title Label - this.titleLabel = this.createTitleLabel(titleArea.getHTMLElement()); + this.titleLabel = this.createTitleLabel(titleArea); // Right Actions Container - $(titleArea).div({ - 'class': 'title-actions' - }, div => { + const titleActionsContainer = append(titleArea, $('.title-actions')); - // Toolbar - this.toolBar = new ToolBar(div.getHTMLElement(), this.contextMenuService, { - actionItemProvider: action => this.actionItemProvider(action as Action), - orientation: ActionsOrientation.HORIZONTAL, - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id) - }); - }); + // Toolbar + this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, { + actionItemProvider: action => this.actionItemProvider(action as Action), + orientation: ActionsOrientation.HORIZONTAL, + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id) + })); - return titleArea.getHTMLElement(); + return titleArea; } protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel { - let titleLabel: Builder; - $(parent).div({ - 'class': 'title-label' - }, div => { - titleLabel = div.span(); - }); + const titleContainer = append(parent, $('.title-label')); + const titleLabel = append(titleContainer, $('h2')); const $this = this; return { updateTitle: (id, title, keybinding) => { - titleLabel.safeInnerHtml(title); - titleLabel.title(keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title); + titleLabel.innerHTML = strings.escape(title); + titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title; }, + updateStyles: () => { - titleLabel.style('color', $this.getColor($this.titleForegroundColor)); + titleLabel.style.color = $this.getColor($this.titleForegroundColor); } }; } @@ -463,21 +457,21 @@ export abstract class CompositePart extends Part { return undefined; } - public createContentArea(parent: HTMLElement): HTMLElement { - return $(parent).div({ - 'class': 'content' - }, div => { - this.progressBar = new ProgressBar(div.getHTMLElement()); - this.toUnbind.push(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.hide(); - }).getHTMLElement(); + createContentArea(parent: HTMLElement): HTMLElement { + const contentContainer = append(parent, $('.content')); + + this.progressBar = this._register(new ProgressBar(contentContainer)); + this._register(attachProgressBarStyler(this.progressBar, this.themeService)); + this.progressBar.hide(); + + return contentContainer; } private onError(error: any): void { this.notificationService.error(types.isString(error) ? new Error(error) : error); } - public getProgressIndicator(id: string): IProgressService { + getProgressIndicator(id: string): IProgressService { return this.mapProgressServiceToComposite[id]; } @@ -489,7 +483,7 @@ export abstract class CompositePart extends Part { return []; } - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { // Pass to super const sizes = super.layout(dimension); @@ -503,13 +497,13 @@ export abstract class CompositePart extends Part { return sizes; } - public shutdown(): void { + shutdown(): void { this.instantiatedComposites.forEach(i => i.shutdown()); super.shutdown(); } - public dispose(): void { + dispose(): void { this.mapCompositeToCompositeContainer = null; this.mapProgressServiceToComposite = null; this.mapActionsBindingToComposite = null; @@ -519,13 +513,8 @@ export abstract class CompositePart extends Part { } this.instantiatedComposites = []; - this.instantiatedCompositeListeners = dispose(this.instantiatedCompositeListeners); - this.progressBar.dispose(); - this.toolBar.dispose(); - - // Super Dispose super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBar.ts b/src/vs/workbench/browser/parts/compositebar/compositeBar.ts deleted file mode 100644 index 54b7a6c74e0..00000000000 --- a/src/vs/workbench/browser/parts/compositebar/compositeBar.ts +++ /dev/null @@ -1,626 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as nls from 'vs/nls'; -import { Action, IAction } from 'vs/base/common/actions'; -import { illegalArgument } from 'vs/base/common/errors'; -import * as arrays from 'vs/base/common/arrays'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IBadge } from 'vs/workbench/services/activity/common/activity'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Widget } from 'vs/base/browser/ui/widget'; - -export interface ICompositeBarOptions { - icon: boolean; - storageId: string; - orientation: ActionsOrientation; - colors: ICompositeBarColors; - compositeSize: number; - overflowActionSize: number; - getActivityAction: (compositeId: string) => ActivityAction; - getCompositePinnedAction: (compositeId: string) => Action; - getOnCompositeClickAction: (compositeId: string) => Action; - getContextMenuActions: () => Action[]; - openComposite: (compositeId: string) => TPromise; - getDefaultCompositeId: () => string; - hidePart: () => TPromise; -} - -export class CompositeBar extends Widget implements ICompositeBar { - - private dimension: Dimension; - - private compositeSwitcherBar: ActionBar; - private compositeOverflowAction: CompositeOverflowActivityAction; - private compositeOverflowActionItem: CompositeOverflowActivityActionItem; - - private model: CompositeBarModel; - private storedState: ISerializedCompositeBarItem[]; - private visibleComposites: string[]; - private compositeSizeInBar: Map; - - constructor( - private options: ICompositeBarOptions, - @IInstantiationService private instantiationService: IInstantiationService, - @IStorageService private storageService: IStorageService, - @IContextMenuService private contextMenuService: IContextMenuService - ) { - super(); - this.model = new CompositeBarModel(options); - this.storedState = this.loadCompositeItemsFromStorage(); - this.visibleComposites = []; - this.compositeSizeInBar = new Map(); - } - - public getCompositesFromStorage(): string[] { - return this.storedState.map(s => s.id); - } - - public create(parent: HTMLElement): HTMLElement { - const actionBarDiv = parent.appendChild($('.composite-bar')); - this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { - actionItemProvider: (action: Action) => { - if (action instanceof CompositeOverflowActivityAction) { - return this.compositeOverflowActionItem; - } - const item = this.model.findItem(action.id); - return item && this.instantiationService.createInstance(CompositeActionItem, action, item.pinnedAction, this.options.colors, this.options.icon, this); - }, - orientation: this.options.orientation, - ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), - animated: false, - })); - - // Contextmenu for composites - this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); - - // Allow to drop at the end to move composites to the end - this._register(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => { - const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); - if (draggedCompositeId) { - EventHelper.stop(e, true); - CompositeActionItem.clearDraggedComposite(); - - const targetItem = this.model.items[this.model.items.length - 1]; - if (targetItem && targetItem.id !== draggedCompositeId) { - this.move(draggedCompositeId, targetItem.id); - } - } - })); - - return actionBarDiv; - } - - public layout(dimension: Dimension): void { - this.dimension = dimension; - if (dimension.height === 0 || dimension.width === 0) { - // Do not layout if not visible. Otherwise the size measurment would be computed wrongly - return; - } - - if (this.compositeSizeInBar.size === 0) { - // Compute size of each composite by getting the size from the css renderer - // Size is later used for overflow computation - this.computeSizes(this.model.items); - } - - this.updateCompositeSwitcher(); - } - - public addComposite({ id, name, order }: { id: string; name: string, order: number }): void { - const state = this.storedState.filter(s => s.id === id)[0]; - const pinned = state ? state.pinned : true; - let index = order >= 0 ? order : this.model.items.length; - - if (state) { - // Find the index by looking its previous item - index = 0; - for (let i = this.storedState.indexOf(state) - 1; i >= 0; i--) { - const previousItemId = this.storedState[i].id; - const previousItemIndex = this.model.findIndex(previousItemId); - if (previousItemIndex !== -1) { - index = previousItemIndex + 1; - break; - } - } - } - - // Add to the model - if (this.model.add(id, name, order, index)) { - this.computeSizes([this.model.findItem(id)]); - if (pinned) { - this.pin(id); - } else { - this.updateCompositeSwitcher(); - } - } - } - - public removeComposite(id: string): void { - - // If it pinned, unpin it first - if (this.isPinned(id)) { - this.unpin(id); - } - - // Remove from the model - if (this.model.remove(id)) { - this.updateCompositeSwitcher(); - } - } - - public activateComposite(id: string): void { - const previousActiveItem = this.model.activeItem; - if (this.model.activate(id)) { - // Update if current composite is neither visible nor pinned - // or previous active composite is not pinned - if (this.visibleComposites.indexOf(id) === - 1 || !this.model.activeItem.pinned || (previousActiveItem && !previousActiveItem.pinned)) { - this.updateCompositeSwitcher(); - } - } - } - - public deactivateComposite(id: string): void { - const previousActiveItem = this.model.activeItem; - if (this.model.deactivate()) { - if (previousActiveItem && !previousActiveItem.pinned) { - this.updateCompositeSwitcher(); - } - } - } - - public showActivity(compositeId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable { - if (!badge) { - throw illegalArgument('badge'); - } - - if (typeof priority !== 'number') { - priority = 0; - } - - const activity: ICompositeActivity = { badge, clazz, priority }; - this.model.addActivity(compositeId, activity); - return toDisposable(() => this.model.removeActivity(compositeId, activity)); - } - - public pin(compositeId: string, open?: boolean): void { - if (this.model.setPinned(compositeId, true)) { - this.updateCompositeSwitcher(); - - if (open) { - this.options.openComposite(compositeId) - .done(() => this.activateComposite(compositeId)); // Activate after opening - } - } - } - - public unpin(compositeId: string): void { - if (this.model.setPinned(compositeId, false)) { - - this.updateCompositeSwitcher(); - - const defaultCompositeId = this.options.getDefaultCompositeId(); - - // Case: composite is not the active one or the active one is a different one - // Solv: we do nothing - if (!this.model.activeItem || this.model.activeItem.id !== compositeId) { - return; - } - - // Deactivate itself - this.deactivateComposite(compositeId); - - // Case: composite is not the default composite and default composite is still showing - // Solv: we open the default composite - if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) { - this.options.openComposite(defaultCompositeId); - } - - // Case: we closed the last visible composite - // Solv: we hide the part - else if (this.visibleComposites.length === 1) { - this.options.hidePart(); - } - - // Case: we closed the default composite - // Solv: we open the next visible composite from top - else { - this.options.openComposite(this.visibleComposites.filter(cid => cid !== compositeId)[0]); - } - - } - - } - - public isPinned(compositeId: string): boolean { - const item = this.model.findItem(compositeId); - return item && item.pinned; - } - - public move(compositeId: string, toCompositeId: string): void { - if (this.model.move(compositeId, toCompositeId)) { - // timeout helps to prevent artifacts from showing up - setTimeout(() => this.updateCompositeSwitcher(), 0); - } - } - - public getAction(compositeId): ActivityAction { - const item = this.model.findItem(compositeId); - return item && item.activityAction; - } - - private computeSizes(items: ICompositeBarItem[]): void { - const size = this.options.compositeSize; - if (size) { - items.forEach(composite => this.compositeSizeInBar.set(composite.id, size)); - } else { - if (this.dimension && this.dimension.height !== 0 && this.dimension.width !== 0) { - // Compute sizes only if visible. Otherwise the size measurment would be computed wrongly. - const currentItemsLength = this.compositeSwitcherBar.items.length; - this.compositeSwitcherBar.push(items.map(composite => composite.activityAction)); - items.map((composite, index) => this.compositeSizeInBar.set(composite.id, this.options.orientation === ActionsOrientation.VERTICAL - ? this.compositeSwitcherBar.getHeight(currentItemsLength + index) - : this.compositeSwitcherBar.getWidth(currentItemsLength + index) - )); - items.forEach(() => this.compositeSwitcherBar.pull(this.compositeSwitcherBar.items.length - 1)); - } - } - } - - private updateCompositeSwitcher(): void { - if (!this.compositeSwitcherBar || !this.dimension) { - return; // We have not been rendered yet so there is nothing to update. - } - - let compositesToShow = this.model.items.filter(item => - item.pinned - || (this.model.activeItem && this.model.activeItem.id === item.id) /* Show the active composite even if it is not pinned */ - ).map(item => item.id); - - // Ensure we are not showing more composites than we have height for - let overflows = false; - let maxVisible = compositesToShow.length; - let size = 0; - const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width; - for (let i = 0; i < compositesToShow.length && size <= limit; i++) { - size += this.compositeSizeInBar.get(compositesToShow[i]); - if (size > limit) { - maxVisible = i; - } - } - overflows = compositesToShow.length > maxVisible; - - if (overflows) { - size -= this.compositeSizeInBar.get(compositesToShow[maxVisible]); - compositesToShow = compositesToShow.slice(0, maxVisible); - size += this.options.overflowActionSize; - } - // Check if we need to make extra room for the overflow action - if (size > limit) { - size -= this.compositeSizeInBar.get(compositesToShow.pop()); - } - - // We always try show the active composite - if (this.model.activeItem && compositesToShow.every(compositeId => compositeId !== this.model.activeItem.id)) { - const removedComposite = compositesToShow.pop(); - size = size - this.compositeSizeInBar.get(removedComposite) + this.compositeSizeInBar.get(this.model.activeItem.id); - compositesToShow.push(this.model.activeItem.id); - } - - // The active composite might have bigger size than the removed composite, check for overflow again - if (size > limit) { - compositesToShow.length ? compositesToShow.splice(compositesToShow.length - 2, 1) : compositesToShow.pop(); - } - - const visibleCompositesChange = !arrays.equals(compositesToShow, this.visibleComposites); - - // Pull out overflow action if there is a composite change so that we can add it to the end later - if (this.compositeOverflowAction && visibleCompositesChange) { - this.compositeSwitcherBar.pull(this.compositeSwitcherBar.length() - 1); - - this.compositeOverflowAction.dispose(); - this.compositeOverflowAction = null; - - this.compositeOverflowActionItem.dispose(); - this.compositeOverflowActionItem = null; - } - - // Pull out composites that overflow or got hidden - const compositesToRemove: number[] = []; - this.visibleComposites.forEach((compositeId, index) => { - if (compositesToShow.indexOf(compositeId) === -1) { - compositesToRemove.push(index); - } - }); - compositesToRemove.reverse().forEach(index => { - const actionItem = this.compositeSwitcherBar.items[index]; - this.compositeSwitcherBar.pull(index); - actionItem.dispose(); - this.visibleComposites.splice(index, 1); - }); - - // Update the positions of the composites - compositesToShow.forEach((compositeId, newIndex) => { - const currentIndex = this.visibleComposites.indexOf(compositeId); - if (newIndex !== currentIndex) { - if (currentIndex !== -1) { - const actionItem = this.compositeSwitcherBar.items[currentIndex]; - this.compositeSwitcherBar.pull(currentIndex); - actionItem.dispose(); - this.visibleComposites.splice(currentIndex, 1); - } - - this.compositeSwitcherBar.push(this.model.findItem(compositeId).activityAction, { label: true, icon: this.options.icon, index: newIndex }); - this.visibleComposites.splice(newIndex, 0, compositeId); - } - }); - - // Add overflow action as needed - if ((visibleCompositesChange && overflows) || this.compositeSwitcherBar.length() === 0) { - this.compositeOverflowAction = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => this.compositeOverflowActionItem.showMenu()); - this.compositeOverflowActionItem = this.instantiationService.createInstance( - CompositeOverflowActivityActionItem, - this.compositeOverflowAction, - () => this.getOverflowingComposites(), - () => this.model.activeItem ? this.model.activeItem.id : void 0, - (compositeId: string) => { - const item = this.model.findItem(compositeId); - return item && item.activity[0] && item.activity[0].badge; - }, - this.options.getOnCompositeClickAction, - this.options.colors - ); - - this.compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true }); - } - - // Persist - this.saveCompositeItems(); - } - - private getOverflowingComposites(): { id: string, name: string }[] { - let overflowingIds = this.model.items.filter(item => item.pinned).map(item => item.id); - - // Show the active composite even if it is not pinned - if (this.model.activeItem && !this.model.activeItem.pinned) { - overflowingIds.push(this.model.activeItem.id); - } - - overflowingIds = overflowingIds.filter(compositeId => this.visibleComposites.indexOf(compositeId) === -1); - return this.model.items.filter(c => overflowingIds.indexOf(c.id) !== -1); - } - - private showContextMenu(e: MouseEvent): void { - EventHelper.stop(e, true); - const event = new StandardMouseEvent(e); - const actions: IAction[] = this.model.items - .map(({ id, name, activityAction }) => ({ - id, - label: name, - checked: this.isPinned(id), - enabled: activityAction.enabled, - run: () => { - if (this.isPinned(id)) { - this.unpin(id); - } else { - this.pin(id, true); - } - } - })); - const otherActions = this.options.getContextMenuActions(); - if (otherActions.length) { - actions.push(new Separator()); - actions.push(...otherActions); - } - this.contextMenuService.showContextMenu({ - getAnchor: () => { return { x: event.posx, y: event.posy }; }, - getActions: () => TPromise.as(actions), - }); - } - - private loadCompositeItemsFromStorage(): ISerializedCompositeBarItem[] { - const storedStates = >JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, '[]')); - const compositeStates = storedStates.map(c => - typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true } : c); - return compositeStates; - } - - private saveCompositeItems(): void { - this.storedState = this.model.toJSON(); - this.storageService.store(this.options.storageId, JSON.stringify(this.storedState), StorageScope.GLOBAL); - } -} - -interface ISerializedCompositeBarItem { - id: string; - pinned: boolean; - order: number; -} - -interface ICompositeBarItem extends ISerializedCompositeBarItem { - name: string; - activityAction: ActivityAction; - pinnedAction: Action; - activity: ICompositeActivity[]; -} - -class CompositeBarModel { - - readonly items: ICompositeBarItem[] = []; - activeItem: ICompositeBarItem; - - constructor(private options: ICompositeBarOptions) { } - - private createCompositeBarItem(id: string, name: string, order: number, pinned: boolean): ICompositeBarItem { - const options = this.options; - return { - id, name, pinned, order, activity: [], - get activityAction() { - return options.getActivityAction(id); - }, - get pinnedAction() { - return options.getCompositePinnedAction(id); - } - }; - } - - add(id: string, name: string, order: number, index: number): boolean { - const item = this.findItem(id); - if (item) { - item.order = order; - item.name = name; - return false; - } else { - if (index === void 0) { - index = 0; - while (index < this.items.length && this.items[index].order < order) { - index++; - } - } - this.items.splice(index, 0, this.createCompositeBarItem(id, name, order, false)); - return true; - } - } - - remove(id: string): boolean { - for (let index = 0; index < this.items.length; index++) { - if (this.items[index].id === id) { - this.items.splice(index, 1); - return true; - } - } - return false; - } - - move(compositeId: string, toCompositeId: string): boolean { - - const fromIndex = this.findIndex(compositeId); - const toIndex = this.findIndex(toCompositeId); - - // Make sure both items are known to the model - if (fromIndex === -1 || toIndex === -1) { - return false; - } - - const sourceItem = this.items.splice(fromIndex, 1)[0]; - this.items.splice(toIndex, 0, sourceItem); - - // Make sure a moved composite gets pinned - sourceItem.pinned = true; - - return true; - } - - setPinned(id: string, pinned: boolean): boolean { - for (let index = 0; index < this.items.length; index++) { - const item = this.items[index]; - if (item.id === id) { - if (item.pinned !== pinned) { - item.pinned = pinned; - return true; - } - return false; - } - } - return false; - } - - addActivity(id: string, activity: ICompositeActivity): boolean { - const item = this.findItem(id); - if (item) { - const stack = item.activity; - for (let i = 0; i <= stack.length; i++) { - if (i === stack.length) { - stack.push(activity); - break; - } else if (stack[i].priority <= activity.priority) { - stack.splice(i, 0, activity); - break; - } - } - this.updateActivity(id); - return true; - } - return false; - } - - removeActivity(id: string, activity: ICompositeActivity): boolean { - const item = this.findItem(id); - if (item) { - const index = item.activity.indexOf(activity); - if (index !== -1) { - item.activity.splice(index, 1); - this.updateActivity(id); - return true; - } - } - return false; - } - - updateActivity(id: string): void { - const item = this.findItem(id); - if (item) { - if (item.activity.length) { - const [{ badge, clazz }] = item.activity; - item.activityAction.setBadge(badge, clazz); - } - else { - item.activityAction.setBadge(undefined); - } - } - } - - activate(id: string): boolean { - if (!this.activeItem || this.activeItem.id !== id) { - if (this.activeItem) { - this.deactivate(); - } - for (let index = 0; index < this.items.length; index++) { - const item = this.items[index]; - if (item.id === id) { - this.activeItem = item; - this.activeItem.activityAction.activate(); - return true; - } - } - } - return false; - } - - deactivate(): boolean { - if (this.activeItem) { - this.activeItem.activityAction.deactivate(); - this.activeItem = void 0; - return true; - } - return false; - } - - findItem(id: string): ICompositeBarItem { - return this.items.filter(item => item.id === id)[0]; - } - - findIndex(id: string): number { - for (let index = 0; index < this.items.length; index++) { - if (this.items[index].id === id) { - return index; - } - } - return -1; - } - - toJSON(): ISerializedCompositeBarItem[] { - return this.items.map(({ id, pinned, order }) => ({ id, pinned, order })); - } -} diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts deleted file mode 100644 index 001dd3a6842..00000000000 --- a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts +++ /dev/null @@ -1,626 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { TPromise } from 'vs/base/common/winjs.base'; -import * as dom from 'vs/base/browser/dom'; -import { Builder, $ } from 'vs/base/browser/builder'; -import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose, IDisposable, empty, toDisposable } from 'vs/base/common/lifecycle'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { DelayedDragHandler } from 'vs/base/browser/dnd'; -import { IActivity } from 'vs/workbench/common/activity'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { Event, Emitter } from 'vs/base/common/event'; - -export interface ICompositeActivity { - badge: IBadge; - clazz: string; - priority: number; -} - -export interface ICompositeBar { - /** - * Unpins a composite from the composite bar. - */ - unpin(compositeId: string): void; - - /** - * Pin a composite inside the composite bar. - */ - pin(compositeId: string): void; - - /** - * Find out if a composite is pinned in the composite bar. - */ - isPinned(compositeId: string): boolean; - - /** - * Reorder composite ordering by moving a composite to the location of another composite. - */ - move(compositeId: string, tocompositeId: string): void; -} - -export class ActivityAction extends Action { - private badge: IBadge; - private clazz: string | undefined; - private _onDidChangeBadge = new Emitter(); - - constructor(private _activity: IActivity) { - super(_activity.id, _activity.name, _activity.cssClass); - - this.badge = null; - } - - public get activity(): IActivity { - return this._activity; - } - - public get onDidChangeBadge(): Event { - return this._onDidChangeBadge.event; - } - - public activate(): void { - if (!this.checked) { - this._setChecked(true); - } - } - - public deactivate(): void { - if (this.checked) { - this._setChecked(false); - } - } - - public getBadge(): IBadge { - return this.badge; - } - - public getClass(): string | undefined { - return this.clazz; - } - - public setBadge(badge: IBadge, clazz?: string): void { - this.badge = badge; - this.clazz = clazz; - this._onDidChangeBadge.fire(this); - } -} - -export interface ICompositeBarColors { - backgroundColor: string; - badgeBackground: string; - badgeForeground: string; - dragAndDropBackground: string; -} - -export interface IActivityActionItemOptions extends IBaseActionItemOptions { - icon?: boolean; - colors: ICompositeBarColors; -} - -export class ActivityActionItem extends BaseActionItem { - protected $container: Builder; - protected $label: Builder; - protected $badge: Builder; - protected options: IActivityActionItemOptions; - - private $badgeContent: Builder; - private badgeDisposable: IDisposable = empty; - private mouseUpTimeout: number; - - constructor( - action: ActivityAction, - options: IActivityActionItemOptions, - @IThemeService protected themeService: IThemeService - ) { - super(null, action, options); - - this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose); - action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose); - } - - protected get activity(): IActivity { - return (this._action as ActivityAction).activity; - } - - protected updateStyles(): void { - const theme = this.themeService.getTheme(); - - // Label - if (this.$label && this.options.icon) { - const background = theme.getColor(this.options.colors.backgroundColor); - - this.$label.style('background-color', background ? background.toString() : null); - } - - // Badge - if (this.$badgeContent) { - const badgeForeground = theme.getColor(this.options.colors.badgeForeground); - const badgeBackground = theme.getColor(this.options.colors.badgeBackground); - const contrastBorderColor = theme.getColor(contrastBorder); - - this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null); - this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null); - - this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null); - this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null); - this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null); - } - } - - public render(container: HTMLElement): void { - super.render(container); - - // Make the container tab-able for keyboard navigation - this.$container = $(container).attr({ - tabIndex: '0', - role: 'button', - title: this.activity.name - }); - - // Try hard to prevent keyboard only focus feedback when using mouse - this.$container.on(dom.EventType.MOUSE_DOWN, () => { - this.$container.addClass('clicked'); - }); - - this.$container.on(dom.EventType.MOUSE_UP, () => { - if (this.mouseUpTimeout) { - clearTimeout(this.mouseUpTimeout); - } - - this.mouseUpTimeout = setTimeout(() => { - this.$container.removeClass('clicked'); - }, 800); // delayed to prevent focus feedback from showing on mouse up - }); - - // Label - this.$label = $('a.action-label').appendTo(this.builder); - if (this.activity.cssClass) { - this.$label.addClass(this.activity.cssClass); - } - if (!this.options.icon) { - this.$label.text(this.getAction().label); - } - - this.$badge = this.builder.clone().div({ 'class': 'badge' }, badge => { - this.$badgeContent = badge.div({ 'class': 'badge-content' }); - }); - - this.$badge.hide(); - - this.updateStyles(); - } - - private onThemeChange(theme: ITheme): void { - this.updateStyles(); - } - - protected updateBadge(badge: IBadge, clazz?: string): void { - if (!this.$badge || !this.$badgeContent) { - return; - } - - this.badgeDisposable.dispose(); - this.badgeDisposable = empty; - - this.$badgeContent.empty(); - this.$badge.hide(); - - if (badge) { - - // Number - if (badge instanceof NumberBadge) { - if (badge.number) { - let number = badge.number.toString(); - if (badge.number > 9999) { - number = nls.localize('largeNumberBadge', '10k+'); - } else if (badge.number > 999) { - number = number.charAt(0) + 'k'; - } - this.$badgeContent.text(number); - this.$badge.show(); - } - } - - // Text - else if (badge instanceof TextBadge) { - this.$badgeContent.text(badge.text); - this.$badge.show(); - } - - // Text - else if (badge instanceof IconBadge) { - this.$badge.show(); - } - - // Progress - else if (badge instanceof ProgressBadge) { - this.$badge.show(); - } - - if (clazz) { - this.$badge.addClass(clazz); - this.badgeDisposable = toDisposable(() => this.$badge.removeClass(clazz)); - } - } - - // Title - let title: string; - if (badge && badge.getDescription()) { - if (this.activity.name) { - title = nls.localize('badgeTitle', "{0} - {1}", this.activity.name, badge.getDescription()); - } else { - title = badge.getDescription(); - } - } else { - title = this.activity.name; - } - - [this.$label, this.$badge, this.$container].forEach(b => { - if (b) { - b.attr('aria-label', title); - b.title(title); - } - }); - } - - private handleBadgeChangeEvenet(): void { - const action = this.getAction(); - if (action instanceof ActivityAction) { - this.updateBadge(action.getBadge(), action.getClass()); - } - } - - public dispose(): void { - super.dispose(); - - if (this.mouseUpTimeout) { - clearTimeout(this.mouseUpTimeout); - } - - this.$badge.destroy(); - } -} - -export class CompositeOverflowActivityAction extends ActivityAction { - - constructor( - private showMenu: () => void - ) { - super({ - id: 'additionalComposites.action', - name: nls.localize('additionalViews', "Additional Views"), - cssClass: 'toggle-more' - }); - } - - public run(event: any): TPromise { - this.showMenu(); - - return TPromise.as(true); - } -} - -export class CompositeOverflowActivityActionItem extends ActivityActionItem { - private actions: Action[]; - - constructor( - action: ActivityAction, - private getOverflowingComposites: () => { id: string, name: string }[], - private getActiveCompositeId: () => string, - private getBadge: (compositeId: string) => IBadge, - private getCompositeOpenAction: (compositeId: string) => Action, - colors: ICompositeBarColors, - @IContextMenuService private contextMenuService: IContextMenuService, - @IThemeService themeService: IThemeService - ) { - super(action, { icon: true, colors }, themeService); - } - - public showMenu(): void { - if (this.actions) { - dispose(this.actions); - } - - this.actions = this.getActions(); - - this.contextMenuService.showContextMenu({ - getAnchor: () => this.builder.getHTMLElement(), - getActions: () => TPromise.as(this.actions), - onHide: () => dispose(this.actions) - }); - } - - private getActions(): Action[] { - return this.getOverflowingComposites().map(composite => { - const action = this.getCompositeOpenAction(composite.id); - action.radio = this.getActiveCompositeId() === action.id; - - const badge = this.getBadge(composite.id); - let suffix: string | number; - if (badge instanceof NumberBadge) { - suffix = badge.number; - } else if (badge instanceof TextBadge) { - suffix = badge.text; - } - - if (suffix) { - action.label = nls.localize('numberBadge', "{0} ({1})", composite.name, suffix); - } else { - action.label = composite.name; - } - - return action; - }); - } - - public dispose(): void { - super.dispose(); - - this.actions = dispose(this.actions); - } -} - -class ManageExtensionAction extends Action { - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension")); - } - - public run(id: string): TPromise { - return this.commandService.executeCommand('_extensions.manage', id); - } -} - -export class CompositeActionItem extends ActivityActionItem { - - private static manageExtensionAction: ManageExtensionAction; - private static draggedCompositeId: string; - - private compositeActivity: IActivity; - private cssClass: string; - - constructor( - private compositeActivityAction: ActivityAction, - private toggleCompositePinnedAction: Action, - colors: ICompositeBarColors, - icon: boolean, - private compositeBar: ICompositeBar, - @IContextMenuService private contextMenuService: IContextMenuService, - @IKeybindingService private keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService - ) { - super(compositeActivityAction, { draggable: true, colors, icon }, themeService); - - this.cssClass = compositeActivityAction.class; - - if (!CompositeActionItem.manageExtensionAction) { - CompositeActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); - } - } - - protected get activity(): IActivity { - if (!this.compositeActivity) { - let activityName: string; - const keybinding = this.getKeybindingLabel(this.compositeActivityAction.activity.keybindingId); - if (keybinding) { - activityName = nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding); - } else { - activityName = this.compositeActivityAction.activity.name; - } - - this.compositeActivity = { - id: this.compositeActivityAction.activity.id, - cssClass: this.cssClass, - name: activityName - }; - } - - return this.compositeActivity; - } - - private getKeybindingLabel(id: string): string { - const kb = this.keybindingService.lookupKeybinding(id); - if (kb) { - return kb.getLabel(); - } - - return null; - } - - public render(container: HTMLElement): void { - super.render(container); - - this._updateChecked(); - this._updateEnabled(); - - this.$container.on('contextmenu', e => { - dom.EventHelper.stop(e, true); - - this.showContextMenu(container); - }); - - // Allow to drag - this.$container.on(dom.EventType.DRAG_START, (e: DragEvent) => { - e.dataTransfer.effectAllowed = 'move'; - this.setDraggedComposite(this.activity.id); - - // Trigger the action even on drag start to prevent clicks from failing that started a drag - if (!this.getAction().checked) { - this.getAction().run(); - } - }); - - // Drag enter - let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470 - this.$container.on(dom.EventType.DRAG_ENTER, (e: DragEvent) => { - const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); - if (draggedCompositeId && draggedCompositeId !== this.activity.id) { - counter++; - this.updateFromDragging(container, true); - } - }); - - // Drag leave - this.$container.on(dom.EventType.DRAG_LEAVE, (e: DragEvent) => { - const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); - if (draggedCompositeId) { - counter--; - if (counter === 0) { - this.updateFromDragging(container, false); - } - } - }); - - // Drag end - this.$container.on(dom.EventType.DRAG_END, (e: DragEvent) => { - const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); - if (draggedCompositeId) { - counter = 0; - this.updateFromDragging(container, false); - - CompositeActionItem.clearDraggedComposite(); - } - }); - - // Drop - this.$container.on(dom.EventType.DROP, (e: DragEvent) => { - dom.EventHelper.stop(e, true); - - const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); - if (draggedCompositeId && draggedCompositeId !== this.activity.id) { - this.updateFromDragging(container, false); - CompositeActionItem.clearDraggedComposite(); - - this.compositeBar.move(draggedCompositeId, this.activity.id); - } - }); - - // Activate on drag over to reveal targets - [this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => { - if (!CompositeActionItem.getDraggedCompositeId() && !this.getAction().checked) { - this.getAction().run(); - } - })); - - this.updateStyles(); - } - - private updateFromDragging(element: HTMLElement, isDragging: boolean): void { - const theme = this.themeService.getTheme(); - const dragBackground = theme.getColor(this.options.colors.dragAndDropBackground); - - element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null; - } - - public static getDraggedCompositeId(): string { - return CompositeActionItem.draggedCompositeId; - } - - private setDraggedComposite(compositeId: string): void { - CompositeActionItem.draggedCompositeId = compositeId; - } - - public static clearDraggedComposite(): void { - CompositeActionItem.draggedCompositeId = void 0; - } - - private showContextMenu(container: HTMLElement): void { - const actions: Action[] = [this.toggleCompositePinnedAction]; - if ((this.compositeActivityAction.activity).extensionId) { - actions.push(new Separator()); - actions.push(CompositeActionItem.manageExtensionAction); - } - - const isPinned = this.compositeBar.isPinned(this.activity.id); - if (isPinned) { - this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide"); - this.toggleCompositePinnedAction.checked = false; - } else { - this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep"); - } - - this.contextMenuService.showContextMenu({ - getAnchor: () => container, - getActionsContext: () => this.activity.id, - getActions: () => TPromise.as(actions) - }); - } - - public focus(): void { - this.$container.domFocus(); - } - - protected _updateClass(): void { - if (this.cssClass) { - this.$label.removeClass(this.cssClass); - } - - this.cssClass = this.getAction().class; - if (this.cssClass) { - this.$label.addClass(this.cssClass); - } - } - - protected _updateChecked(): void { - if (this.getAction().checked) { - this.$container.addClass('checked'); - } else { - this.$container.removeClass('checked'); - } - } - - protected _updateEnabled(): void { - if (this.getAction().enabled) { - this.builder.removeClass('disabled'); - } else { - this.builder.addClass('disabled'); - } - } - - public dispose(): void { - super.dispose(); - - CompositeActionItem.clearDraggedComposite(); - - this.$label.destroy(); - } -} - -export class ToggleCompositePinnedAction extends Action { - - constructor( - private activity: IActivity, - private compositeBar: ICompositeBar - ) { - super('show.toggleCompositePinned', activity ? activity.name : nls.localize('toggle', "Toggle View Pinned")); - - this.checked = this.activity && this.compositeBar.isPinned(this.activity.id); - } - - public run(context: string): TPromise { - const id = this.activity ? this.activity.id : context; - - if (this.compositeBar.isPinned(id)) { - this.compositeBar.unpin(id); - } else { - this.compositeBar.pin(id); - } - - return TPromise.as(true); - } -} diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 1a18aa8f6f4..2d62841051a 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -6,11 +6,18 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Panel } from 'vs/workbench/browser/panel'; -import { EditorInput, EditorOptions, IEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditor, GroupIdentifier, IEditorMemento } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { LRUCache } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { once, Event } from 'vs/base/common/event'; +import { isEmptyObject } from 'vs/base/common/types'; +import { DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; +import { Scope } from 'vs/workbench/common/memento'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -27,6 +34,15 @@ import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsSer */ export abstract class BaseEditor extends Panel implements IEditor { + private static readonly EDITOR_MEMENTOS: Map> = new Map>(); + + readonly minimumWidth = DEFAULT_EDITOR_MIN_DIMENSIONS.width; + readonly maximumWidth = DEFAULT_EDITOR_MAX_DIMENSIONS.width; + readonly minimumHeight = DEFAULT_EDITOR_MIN_DIMENSIONS.height; + readonly maximumHeight = DEFAULT_EDITOR_MAX_DIMENSIONS.height; + + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = Event.None; + protected _input: EditorInput; private _options: EditorOptions; @@ -128,18 +144,161 @@ export abstract class BaseEditor extends Panel implements IEditor { this._group = group; } - /** - * Subclasses can set this to false if it does not make sense to center editor input. - */ - supportsCenteredLayout(): boolean { - return true; + protected getEditorMemento(storageService: IStorageService, editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + const mementoKey = `${this.getId()}${key}`; + + let editorMemento = BaseEditor.EDITOR_MEMENTOS.get(mementoKey); + if (!editorMemento) { + editorMemento = new EditorMemento(this.getId(), key, this.getMemento(storageService, Scope.WORKSPACE), limit, editorGroupService); + BaseEditor.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + } + + return editorMemento; + } + + shutdown(): void { + + // Shutdown all editor memento for this editor type + BaseEditor.EDITOR_MEMENTOS.forEach(editorMemento => { + if (editorMemento.id === this.getId()) { + editorMemento.shutdown(); + } + }); + + super.shutdown(); } dispose(): void { this._input = null; this._options = null; - // Super Dispose super.dispose(); } } + +interface MapGroupToMemento { + [group: number]: T; +} + +export class EditorMemento implements IEditorMemento { + private cache: LRUCache>; + private cleanedUp = false; + + constructor( + private _id: string, + private key: string, + private memento: object, + private limit: number, + private editorGroupService: IEditorGroupsService + ) { } + + get id(): string { + return this._id; + } + + saveState(group: IEditorGroup, resource: URI, state: T): void; + saveState(group: IEditorGroup, editor: EditorInput, state: T): void; + saveState(group: IEditorGroup, resourceOrEditor: URI | EditorInput, state: T): void { + const resource = this.doGetResource(resourceOrEditor); + if (!resource || !group) { + return; // we are not in a good state to save any state for a resource + } + + const cache = this.doLoad(); + + let mementoForResource = cache.get(resource.toString()); + if (!mementoForResource) { + mementoForResource = Object.create(null) as MapGroupToMemento; + cache.set(resource.toString(), mementoForResource); + } + + mementoForResource[group.id] = state; + + // Automatically clear when editor input gets disposed if any + if (resourceOrEditor instanceof EditorInput) { + once(resourceOrEditor.onDispose)(() => { + this.clearState(resource); + }); + } + } + + loadState(group: IEditorGroup, resource: URI): T; + loadState(group: IEditorGroup, editor: EditorInput): T; + loadState(group: IEditorGroup, resourceOrEditor: URI | EditorInput): T { + const resource = this.doGetResource(resourceOrEditor); + if (!resource || !group) { + return void 0; // we are not in a good state to load any state for a resource + } + + const cache = this.doLoad(); + + const mementoForResource = cache.get(resource.toString()); + if (mementoForResource) { + return mementoForResource[group.id]; + } + + return void 0; + } + + clearState(resource: URI): void; + clearState(editor: EditorInput): void; + clearState(resourceOrEditor: URI | EditorInput): void { + const resource = this.doGetResource(resourceOrEditor); + if (resource) { + const cache = this.doLoad(); + cache.delete(resource.toString()); + } + } + + private doGetResource(resourceOrEditor: URI | EditorInput): URI { + if (resourceOrEditor instanceof EditorInput) { + return resourceOrEditor.getResource(); + } + + return resourceOrEditor; + } + + private doLoad(): LRUCache> { + if (!this.cache) { + this.cache = new LRUCache>(this.limit); + + // Restore from serialized map state + const rawEditorMemento = this.memento[this.key]; + if (Array.isArray(rawEditorMemento)) { + this.cache.fromJSON(rawEditorMemento); + } + } + + return this.cache; + } + + shutdown(): void { + const cache = this.doLoad(); + + // Cleanup once during shutdown + if (!this.cleanedUp) { + this.cleanUp(); + this.cleanedUp = true; + } + + this.memento[this.key] = cache.toJSON(); + } + + private cleanUp(): void { + const cache = this.doLoad(); + + // Remove groups from states that no longer exist + cache.forEach((mapGroupToMemento, resource) => { + Object.keys(mapGroupToMemento).forEach(group => { + const groupId: GroupIdentifier = Number(group); + if (!this.editorGroupService.getGroup(groupId)) { + delete mapGroupToMemento[groupId]; + + if (isEmptyObject(mapGroupToMemento)) { + cache.delete(resource); + } + } + }); + }); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index 8383ebf5b42..203fb0368aa 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -18,7 +18,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina */ export class BinaryResourceDiffEditor extends SideBySideEditor { - public static readonly ID = BINARY_DIFF_EDITOR_ID; + static readonly ID = BINARY_DIFF_EDITOR_ID; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -28,7 +28,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { super(telemetryService, instantiationService, themeService); } - public getMetadata(): string { + getMetadata(): string { const master = this.masterEditor; const details = this.detailsEditor; diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 47de56f9791..1fdec9309cc 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -8,7 +8,6 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Builder, $ } from 'vs/base/browser/builder'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -17,10 +16,11 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceViewerContext, ResourceViewer } from 'vs/workbench/browser/parts/editor/resourceViewer'; -import URI from 'vs/base/common/uri'; -import { Dimension } from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { Dimension, size, clearNode } from 'vs/base/browser/dom'; import { IFileService } from 'vs/platform/files/common/files'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { dispose } from 'vs/base/common/lifecycle'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: EditorOptions) => void; @@ -32,11 +32,12 @@ export interface IOpenCallbacks { */ export abstract class BaseBinaryResourceEditor extends BaseEditor { - private readonly _onMetadataChanged: Emitter; + private readonly _onMetadataChanged: Emitter = this._register(new Emitter()); + get onMetadataChanged(): Event { return this._onMetadataChanged.event; } private callbacks: IOpenCallbacks; private metadata: string; - private binaryContainer: Builder; + private binaryContainer: HTMLElement; private scrollbar: DomScrollableElement; private resourceViewerContext: ResourceViewerContext; @@ -49,37 +50,29 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { ) { super(id, telemetryService, themeService); - this._onMetadataChanged = new Emitter(); - this.toUnbind.push(this._onMetadataChanged); - this.callbacks = callbacks; } - public get onMetadataChanged(): Event { - return this._onMetadataChanged.event; - } - - public getTitle(): string { + getTitle(): string { return this.input ? this.input.getName() : nls.localize('binaryEditor', "Binary Viewer"); } protected createEditor(parent: HTMLElement): void { // Container for Binary - const binaryContainerElement = document.createElement('div'); - binaryContainerElement.className = 'binary-container'; - this.binaryContainer = $(binaryContainerElement); - this.binaryContainer.style('outline', 'none'); - this.binaryContainer.tabindex(0); // enable focus support from the editor part (do not remove) + this.binaryContainer = document.createElement('div'); + this.binaryContainer.className = 'binary-container'; + this.binaryContainer.style.outline = 'none'; + this.binaryContainer.tabIndex = 0; // enable focus support from the editor part (do not remove) // Custom Scrollbars - this.scrollbar = new DomScrollableElement(binaryContainerElement, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto }); + this.scrollbar = this._register(new DomScrollableElement(this.binaryContainer, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto })); parent.appendChild(this.scrollbar.getDomNode()); } - public setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(model => { + return input.resolve().then(model => { // Check for cancellation if (token.isCancellationRequested) { @@ -95,7 +88,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { this.resourceViewerContext = ResourceViewer.show( { name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, this._fileService, - this.binaryContainer.getHTMLElement(), + this.binaryContainer, this.scrollbar, resource => this.callbacks.openInternal(input, options), resource => this.callbacks.openExternal(resource), @@ -109,47 +102,44 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { private handleMetadataChanged(meta: string): void { this.metadata = meta; + this._onMetadataChanged.fire(); } - public getMetadata(): string { + getMetadata(): string { return this.metadata; } - public supportsCenteredLayout(): boolean { - return false; - } - - public clearInput(): void { + clearInput(): void { // Clear Meta this.handleMetadataChanged(null); - // Empty HTML Container - $(this.binaryContainer).empty(); + // Clear Resource Viewer + clearNode(this.binaryContainer); + this.resourceViewerContext = dispose(this.resourceViewerContext); super.clearInput(); } - public layout(dimension: Dimension): void { + layout(dimension: Dimension): void { // Pass on to Binary Container - this.binaryContainer.size(dimension.width, dimension.height); + size(this.binaryContainer, dimension.width, dimension.height); this.scrollbar.scanDomNode(); - if (this.resourceViewerContext) { + if (this.resourceViewerContext && this.resourceViewerContext.layout) { this.resourceViewerContext.layout(dimension); } } - public focus(): void { - this.binaryContainer.domFocus(); + focus(): void { + this.binaryContainer.focus(); } - public dispose(): void { + dispose(): void { + this.binaryContainer.remove(); - // Destroy Container - this.binaryContainer.destroy(); - this.scrollbar.dispose(); + this.resourceViewerContext = dispose(this.resourceViewerContext); super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts new file mode 100644 index 00000000000..5a11594b7d8 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { BreadcrumbsWidget } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { Emitter, Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { GroupIdentifier } from 'vs/workbench/common/editor'; + +export const IBreadcrumbsService = createDecorator('IEditorBreadcrumbsService'); + +export interface IBreadcrumbsService { + + _serviceBrand: any; + + register(group: GroupIdentifier, widget: BreadcrumbsWidget): IDisposable; + + getWidget(group: GroupIdentifier): BreadcrumbsWidget; +} + + +export class BreadcrumbsService implements IBreadcrumbsService { + + _serviceBrand: any; + + private readonly _map = new Map(); + + register(group: number, widget: BreadcrumbsWidget): IDisposable { + if (this._map.has(group)) { + throw new Error(`group (${group}) has already a widget`); + } + this._map.set(group, widget); + return { + dispose: () => this._map.delete(group) + }; + } + + getWidget(group: number): BreadcrumbsWidget { + return this._map.get(group); + } +} + +registerSingleton(IBreadcrumbsService, BreadcrumbsService); + + +//#region config + +export abstract class BreadcrumbsConfig { + + name: string; + onDidChange: Event; + + abstract getValue(overrides?: IConfigurationOverrides): T; + abstract updateValue(value: T, overrides?: IConfigurationOverrides): Thenable; + abstract dispose(): void; + + private constructor() { + // internal + } + + static IsEnabled = BreadcrumbsConfig._stub('breadcrumbs.enabled'); + static UseQuickPick = BreadcrumbsConfig._stub('breadcrumbs.useQuickPick'); + static FilePath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.filePath'); + static SymbolPath = BreadcrumbsConfig._stub<'on' | 'off' | 'last'>('breadcrumbs.symbolPath'); + static FilterOnType = BreadcrumbsConfig._stub('breadcrumbs.filterOnType'); + + static FileExcludes = BreadcrumbsConfig._stub('files.exclude'); + + private static _stub(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig } { + return { + bindTo(service) { + let onDidChange = new Emitter(); + + let listener = service.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(name)) { + onDidChange.fire(undefined); + } + }); + + return new class implements BreadcrumbsConfig{ + readonly name = name; + readonly onDidChange = onDidChange.event; + getValue(overrides?: IConfigurationOverrides): T { + return service.getValue(name, overrides); + } + updateValue(newValue: T, overrides?: IConfigurationOverrides): Thenable { + return service.updateValue(name, newValue, overrides); + } + dispose(): void { + listener.dispose(); + onDidChange.dispose(); + } + }; + } + }; + } +} + +Registry.as(Extensions.Configuration).registerConfiguration({ + id: 'breadcrumbs', + title: localize('title', "Breadcrumb Navigation"), + order: 101, + type: 'object', + properties: { + 'breadcrumbs.enabled': { + description: localize('enabled', "Enable/disable navigation breadcrumbs"), + type: 'boolean', + default: false + }, + // 'breadcrumbs.useQuickPick': { + // description: localize('useQuickPick', "Use quick pick instead of breadcrumb-pickers."), + // type: 'boolean', + // default: false + // }, + 'breadcrumbs.filePath': { + description: localize('filepath', "Controls whether and how file paths are shown in the breadcrumbs view."), + type: 'string', + default: 'on', + enum: ['on', 'off', 'last'], + enumDescriptions: [ + localize('filepath.on', "Show the file path in the breadcrumbs view."), + localize('filepath.off', "Do not show the file path in the breadcrumbs view."), + localize('filepath.last', "Only show the last element of the file path in the breadcrumbs view."), + ] + }, + 'breadcrumbs.symbolPath': { + description: localize('symbolpath', "Controls whether and how symbols are shown in the breadcrumbs view."), + type: 'string', + default: 'on', + enum: ['on', 'off', 'last'], + enumDescriptions: [ + localize('symbolpath.on', "Show all symbols in the breadcrumbs view."), + localize('symbolpath.off', "Do not show symbols in the breadcrumbs view."), + localize('symbolpath.last', "Only show the current symbol in the breadcrumbs view."), + ] + }, + // 'breadcrumbs.filterOnType': { + // description: localize('filterOnType', "Controls whether the breadcrumb picker filters or highlights when typing."), + // type: 'boolean', + // default: false + // }, + } +}); + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts new file mode 100644 index 00000000000..e08f3dc0ea3 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -0,0 +1,607 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as dom from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { tail } from 'vs/base/common/arrays'; +import { timeout } from 'vs/base/common/async'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { combinedDisposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/breadcrumbscontrol'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; +import { symbolKindToCssClass } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry'; +import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; +import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; + +class Item extends BreadcrumbsItem { + + private readonly _disposables: IDisposable[] = []; + + constructor( + readonly element: BreadcrumbElement, + readonly options: IBreadcrumbsControlOptions, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + } + + dispose(): void { + dispose(this._disposables); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof Item)) { + return false; + } + if (this.element instanceof FileElement && other.element instanceof FileElement) { + return isEqual(this.element.uri, other.element.uri); + } + if (this.element instanceof TreeElement && other.element instanceof TreeElement) { + return this.element.id === other.element.id; + } + return false; + } + + render(container: HTMLElement): void { + if (this.element instanceof FileElement) { + // file/folder + let label = this._instantiationService.createInstance(FileLabel, container, {}); + label.setFile(this.element.uri, { + hidePath: true, + hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, + fileKind: this.element.kind, + fileDecorations: { colors: this.options.showDecorationColors, badges: false }, + }); + dom.addClass(container, FileKind[this.element.kind].toLowerCase()); + this._disposables.push(label); + + } else if (this.element instanceof OutlineModel) { + // has outline element but not in one + let label = document.createElement('div'); + label.innerHTML = '…'; + label.className = 'hint-more'; + container.appendChild(label); + + } else if (this.element instanceof OutlineGroup) { + // provider + let label = new IconLabel(container); + label.setValue(this.element.provider.displayName); + this._disposables.push(label); + + } else if (this.element instanceof OutlineElement) { + // symbol + if (this.options.showSymbolIcons) { + let icon = document.createElement('div'); + icon.className = symbolKindToCssClass(this.element.symbol.kind); + container.appendChild(icon); + dom.addClass(container, 'shows-symbol-icon'); + } + let label = new IconLabel(container); + let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE'); + label.setValue(title); + this._disposables.push(label); + } + } +} + +export interface IBreadcrumbsControlOptions { + showFileIcons: boolean; + showSymbolIcons: boolean; + showDecorationColors: boolean; + breadcrumbsBackground: ColorIdentifier | ColorFunction; +} + +export class BreadcrumbsControl { + + static HEIGHT = 22; + + static readonly Payload_Reveal = {}; + static readonly Payload_RevealAside = {}; + static readonly Payload_Pick = {}; + + static CK_BreadcrumbsPossible = new RawContextKey('breadcrumbsPossible', false); + static CK_BreadcrumbsVisible = new RawContextKey('breadcrumbsVisible', false); + static CK_BreadcrumbsActive = new RawContextKey('breadcrumbsActive', false); + + private readonly _ckBreadcrumbsPossible: IContextKey; + private readonly _ckBreadcrumbsVisible: IContextKey; + private readonly _ckBreadcrumbsActive: IContextKey; + + private readonly _cfUseQuickPick: BreadcrumbsConfig; + + readonly domNode: HTMLDivElement; + private readonly _widget: BreadcrumbsWidget; + + private _disposables = new Array(); + private _breadcrumbsDisposables = new Array(); + private _breadcrumbsPickerShowing = false; + + constructor( + container: HTMLElement, + private readonly _options: IBreadcrumbsControlOptions, + private readonly _editorGroup: EditorGroupView, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, + @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, + @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, + ) { + this.domNode = document.createElement('div'); + dom.addClass(this.domNode, 'breadcrumbs-control'); + dom.append(container, this.domNode); + + this._widget = new BreadcrumbsWidget(this.domNode); + this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables); + this._widget.onDidFocusItem(this._onFocusEvent, this, this._disposables); + this._widget.onDidChangeFocus(this._updateCkBreadcrumbsActive, this, this._disposables); + this._disposables.push(attachBreadcrumbsStyler(this._widget, this._themeService, { breadcrumbsBackground: _options.breadcrumbsBackground })); + + this._ckBreadcrumbsPossible = BreadcrumbsControl.CK_BreadcrumbsPossible.bindTo(this._contextKeyService); + this._ckBreadcrumbsVisible = BreadcrumbsControl.CK_BreadcrumbsVisible.bindTo(this._contextKeyService); + this._ckBreadcrumbsActive = BreadcrumbsControl.CK_BreadcrumbsActive.bindTo(this._contextKeyService); + + this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); + + this._disposables.push(breadcrumbsService.register(this._editorGroup.id, this._widget)); + } + + dispose(): void { + this._disposables = dispose(this._disposables); + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + this._ckBreadcrumbsPossible.reset(); + this._ckBreadcrumbsVisible.reset(); + this._ckBreadcrumbsActive.reset(); + this._cfUseQuickPick.dispose(); + this._widget.dispose(); + this.domNode.remove(); + } + + layout(dim: dom.Dimension): void { + this._widget.layout(dim); + } + + isHidden(): boolean { + return dom.hasClass(this.domNode, 'hidden'); + } + + hide(): void { + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + this._ckBreadcrumbsVisible.set(false); + dom.toggleClass(this.domNode, 'hidden', true); + } + + update(): boolean { + this._breadcrumbsDisposables = dispose(this._breadcrumbsDisposables); + + // honor diff editors and such + let input = this._editorGroup.activeEditor; + if (input instanceof SideBySideEditorInput) { + input = input.master; + } + + if (!input || !input.getResource() || (input.getResource().scheme !== Schemas.untitled && !this._fileService.canHandleResource(input.getResource()))) { + // cleanup and return when there is no input or when + // we cannot handle this input + this._ckBreadcrumbsPossible.set(false); + if (!this.isHidden()) { + this.hide(); + return true; + } else { + return false; + } + } + + dom.toggleClass(this.domNode, 'hidden', false); + this._ckBreadcrumbsVisible.set(true); + this._ckBreadcrumbsPossible.set(true); + + let editor = this._getActiveCodeEditor(); + let model = new EditorBreadcrumbsModel(input.getResource(), editor, this._workspaceService, this._configurationService); + dom.toggleClass(this.domNode, 'relative-path', model.isRelative()); + + let updateBreadcrumbs = () => { + let items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); + this._widget.setItems(items); + this._widget.reveal(items[items.length - 1]); + }; + let listener = model.onDidUpdate(updateBreadcrumbs); + updateBreadcrumbs(); + this._breadcrumbsDisposables = [model, listener]; + + // close picker on hide/update + this._breadcrumbsDisposables.push({ + dispose: () => { + if (this._breadcrumbsPickerShowing) { + this._contextViewService.hideContextView(this); + } + } + }); + + return true; + } + + private _getActiveCodeEditor(): ICodeEditor { + let control = this._editorGroup.activeControl.getControl(); + let editor: ICodeEditor; + if (isCodeEditor(control)) { + editor = control as ICodeEditor; + } else if (isDiffEditor(control)) { + editor = control.getModifiedEditor(); + } + return editor; + } + + private _onFocusEvent(event: IBreadcrumbsItemEvent): void { + if (event.item && this._breadcrumbsPickerShowing) { + return this._widget.setSelection(event.item); + } + } + + private _onSelectEvent(event: IBreadcrumbsItemEvent): void { + if (!event.item) { + return; + } + + this._editorGroup.focus(); + const { element } = event.item as Item; + + const group = this._getEditorGroup(event.payload); + if (group !== undefined) { + // reveal the item + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + this._revealInEditor(event, element, group); + return; + } + + if (this._cfUseQuickPick.getValue()) { + // using quick pick + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + this._quickOpenService.show(element instanceof TreeElement ? '@' : ''); + return; + } + + // show picker + let picker: BreadcrumbsPicker; + let editor = this._getActiveCodeEditor(); + let editorDecorations: string[] = []; + let editorViewState: ICodeEditorViewState; + + this._contextViewService.showContextView({ + render: (parent: HTMLElement) => { + picker = createBreadcrumbsPicker(this._instantiationService, parent, element); + let selectListener = picker.onDidPickElement(data => { + if (data.target) { + editorViewState = undefined; + } + this._contextViewService.hideContextView(this); + this._revealInEditor(event, data.target, this._getEditorGroup(data.payload && data.payload.originalEvent)); + }); + let focusListener = picker.onDidFocusElement(data => { + if (!editor || !(data.target instanceof OutlineElement)) { + return; + } + if (!editorViewState) { + editorViewState = editor.saveViewState(); + } + const { symbol } = data.target; + editor.revealRangeInCenter(symbol.range, ScrollType.Smooth); + editorDecorations = editor.deltaDecorations(editorDecorations, [{ + range: symbol.range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }]); + + }); + this._breadcrumbsPickerShowing = true; + this._updateCkBreadcrumbsActive(); + + return combinedDisposable([selectListener, focusListener, picker]); + }, + getAnchor: () => { + let maxInnerWidth = window.innerWidth - 8 /*a little less the the full widget*/; + let pickerHeight = Math.min(330, window.innerHeight * 0.4); + let pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17)); + let pickerArrowSize = 8; + let pickerArrowOffset: number; + + let data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement); + let y = data.top + data.height - pickerArrowSize; + let x = data.left; + if (x + pickerWidth >= maxInnerWidth) { + x = maxInnerWidth - pickerWidth; + } + if (event.payload instanceof StandardMouseEvent) { + let maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize; + pickerArrowOffset = event.payload.posx - x; + if (pickerArrowOffset > maxPickerArrowOffset) { + x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset); + pickerArrowOffset = maxPickerArrowOffset; + } + } else { + pickerArrowOffset = (data.left + (data.width * .3)) - x; + } + picker.setInput(element, pickerHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset)); + return { x, y }; + }, + onHide: (data) => { + if (editor) { + editor.deltaDecorations(editorDecorations, []); + if (editorViewState) { + editor.restoreViewState(editorViewState); + } + } + this._breadcrumbsPickerShowing = false; + this._updateCkBreadcrumbsActive(); + if (data === this) { + this._widget.setFocused(undefined); + this._widget.setSelection(undefined); + } + } + }); + } + + private _updateCkBreadcrumbsActive(): void { + const value = this._widget.isDOMFocused() || this._breadcrumbsPickerShowing; + this._ckBreadcrumbsActive.set(value); + } + + private _revealInEditor(event: IBreadcrumbsItemEvent, element: any, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): void { + if (element instanceof FileElement) { + if (element.kind === FileKind.FILE) { + // open file in editor + this._editorService.openEditor({ resource: element.uri }, group); + } else { + // show next picker + let items = this._widget.getItems(); + let idx = items.indexOf(event.item); + this._widget.setFocused(items[idx + 1]); + this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); + } + + } else if (element instanceof OutlineElement) { + // open symbol in editor + let model = OutlineModel.get(element); + this._editorService.openEditor({ + resource: model.textModel.uri, + options: { + selection: Range.collapseToStart(element.symbol.selectionRange), + revealInCenterIfOutsideViewport: true + } + }, group); + } + } + + private _getEditorGroup(data: StandardMouseEvent | object): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined { + if (data === BreadcrumbsControl.Payload_RevealAside || (data instanceof StandardMouseEvent && data.altKey)) { + return SIDE_GROUP; + } else if (data === BreadcrumbsControl.Payload_Reveal || (data instanceof StandardMouseEvent && data.metaKey)) { + return ACTIVE_GROUP; + } else { + return undefined; + } + } +} + +//#region commands + +// toggle command +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'breadcrumbs.toggle', + title: localize('cmd.toggle', "Toggle Breadcrumbs"), + category: localize('cmd.category', "View") + } +}); +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + order: 99, + command: { + id: 'breadcrumbs.toggle', + title: localize('miToggleBreadcrumbs', "Toggle &&Breadcrumbs") + } +}); +CommandsRegistry.registerCommand('breadcrumbs.toggle', accessor => { + let config = accessor.get(IConfigurationService); + let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); + BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); +}); + +// focus/focus-and-select +function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): void { + // find widget and focus/select + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + if (widget) { + const item = tail(widget.getItems()); + widget.setFocused(item); + if (select) { + widget.setSelection(item, BreadcrumbsControl.Payload_Pick); + } + } +} +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'breadcrumbs.focusAndSelect', + title: localize('cmd.focus', "Focus Breadcrumbs"), + precondition: BreadcrumbsControl.CK_BreadcrumbsVisible + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusAndSelect', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: BreadcrumbsControl.CK_BreadcrumbsPossible, + handler: accessor => focusAndSelectHandler(accessor, true) +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focus', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_SEMICOLON, + when: BreadcrumbsControl.CK_BreadcrumbsPossible, + handler: accessor => focusAndSelectHandler(accessor, false) +}); + +// this commands is only enabled when breadcrumbs are +// disabled which it then enables and focuses +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.toggleToOn', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + when: ContextKeyExpr.not('config.breadcrumbs.enabled'), + handler: async accessor => { + const instant = accessor.get(IInstantiationService); + const config = accessor.get(IConfigurationService); + // check if enabled and iff not enable + const isEnabled = BreadcrumbsConfig.IsEnabled.bindTo(config); + if (!isEnabled.getValue()) { + await isEnabled.updateValue(true); + await timeout(50); // hacky - the widget might not be ready yet... + } + return instant.invokeFunction(focusAndSelectHandler, true); + } +}); + +// navigation +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusNext', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.RightArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.RightArrow], + mac: { + primary: KeyCode.RightArrow, + secondary: [KeyMod.Alt | KeyCode.RightArrow], + }, + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusNext(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.focusPrevious', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.LeftArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.LeftArrow], + mac: { + primary: KeyCode.LeftArrow, + secondary: [KeyMod.Alt | KeyCode.LeftArrow], + }, + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).focusPrev(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectFocused', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Enter, + secondary: [KeyCode.DownArrow], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Pick); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.revealFocused', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Space, + secondary: [KeyMod.CtrlCmd | KeyCode.Enter], + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + const widget = breadcrumbs.getWidget(groups.activeGroup.id); + widget.setSelection(widget.getFocused(), BreadcrumbsControl.Payload_Reveal); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.selectEditor', + weight: KeybindingWeight.WorkbenchContrib + 1, + primary: KeyCode.Escape, + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive), + handler(accessor) { + const groups = accessor.get(IEditorGroupsService); + const breadcrumbs = accessor.get(IBreadcrumbsService); + breadcrumbs.getWidget(groups.activeGroup.id).setFocused(undefined); + breadcrumbs.getWidget(groups.activeGroup.id).setSelection(undefined); + groups.activeGroup.activeControl.focus(); + } +}); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'breadcrumbs.revealFocusedFromTreeAside', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey), + handler(accessor) { + const editors = accessor.get(IEditorService); + const lists = accessor.get(IListService); + const element = lists.lastFocusedList.getFocus(); + if (element instanceof OutlineElement) { + // open symbol in editor + return editors.openEditor({ + resource: OutlineModel.get(element).textModel.uri, + options: { selection: Range.collapseToStart(element.symbol.selectionRange) } + }, SIDE_GROUP); + + } else if (URI.isUri(element.resource)) { + // open file in editor + return editors.openEditor({ + resource: element.resource, + }, SIDE_GROUP); + + } else { + // ignore + return undefined; + } + } +}); +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts new file mode 100644 index 00000000000..6459aadebb3 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { size } from 'vs/base/common/collections'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { isEqual, dirname } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IPosition } from 'vs/editor/common/core/position'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { Schemas } from 'vs/base/common/network'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { FileKind } from 'vs/platform/files/common/files'; + +export class FileElement { + constructor( + readonly uri: URI, + readonly kind: FileKind + ) { } +} + +export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; + +type FileInfo = { path: FileElement[], folder: IWorkspaceFolder }; + +export class EditorBreadcrumbsModel { + + private readonly _disposables: IDisposable[] = []; + private readonly _fileInfo: FileInfo; + + private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; + private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; + + private _outlineElements: (OutlineModel | OutlineGroup | OutlineElement)[] = []; + private _outlineDisposables: IDisposable[] = []; + + private _onDidUpdate = new Emitter(); + readonly onDidUpdate: Event = this._onDidUpdate.event; + + constructor( + private readonly _uri: URI, + private readonly _editor: ICodeEditor | undefined, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + @IConfigurationService configurationService: IConfigurationService, + ) { + + this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); + this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService); + + this._disposables.push(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); + this._disposables.push(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); + + this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(this._uri, workspaceService); + this._bindToEditor(); + this._onDidUpdate.fire(this); + } + + dispose(): void { + this._cfgFilePath.dispose(); + this._cfgSymbolPath.dispose(); + dispose(this._disposables); + } + + isRelative(): boolean { + return Boolean(this._fileInfo.folder); + } + + getElements(): ReadonlyArray { + let result: BreadcrumbElement[] = []; + + // file path elements + if (this._cfgFilePath.getValue() === 'on') { + result = result.concat(this._fileInfo.path); + } else if (this._cfgFilePath.getValue() === 'last' && this._fileInfo.path.length > 0) { + result = result.concat(this._fileInfo.path.slice(-1)); + } + + // symbol path elements + if (this._cfgSymbolPath.getValue() === 'on') { + result = result.concat(this._outlineElements); + } else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) { + result = result.concat(this._outlineElements.slice(-1)); + } + + return result; + } + + private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo { + + if (uri.scheme === Schemas.untitled) { + return { + folder: undefined, + path: [] + }; + } + + let info: FileInfo = { + folder: workspaceService.getWorkspaceFolder(uri), + path: [] + }; + + while (uri.path !== '/') { + if (info.folder && isEqual(info.folder.uri, uri)) { + break; + } + info.path.unshift(new FileElement(uri, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER)); + uri = dirname(uri); + } + + if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + info.path.unshift(new FileElement(info.folder.uri, FileKind.ROOT_FOLDER)); + } + return info; + } + + private _bindToEditor(): void { + if (!this._editor) { + return; + } + // update as model changes + this._disposables.push(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModel(_ => this._updateOutline())); + this._disposables.push(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); + this._disposables.push(debounceEvent(this._editor.onDidChangeModelContent, _ => _, 350)(_ => this._updateOutline(true))); + this._updateOutline(); + + // stop when editor dies + this._disposables.push(this._editor.onDidDispose(() => this._outlineDisposables = dispose(this._outlineDisposables))); + } + + private _updateOutline(didChangeContent?: boolean): void { + + this._outlineDisposables = dispose(this._outlineDisposables); + if (!didChangeContent) { + this._updateOutlineElements([]); + } + + const buffer = this._editor.getModel(); + if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { + return; + } + + const source = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeout = new TimeoutTimer(); + + this._outlineDisposables.push({ + dispose: () => { + source.cancel(); + source.dispose(); + timeout.dispose(); + } + }); + + OutlineModel.create(buffer, source.token).then(model => { + if (TreeElement.empty(model)) { + // empty -> no outline elements + this._updateOutlineElements([]); + + } else { + // copy the model + model = model.adopt(); + + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + this._outlineDisposables.push(this._editor.onDidChangeCursorPosition(_ => { + timeout.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.getModel()) { + this._updateOutlineElements(this._getOutlineElements(model, this._editor.getPosition())); + } + }, 150); + })); + } + }).catch(err => { + this._updateOutlineElements([]); + onUnexpectedError(err); + }); + } + + private _getOutlineElements(model: OutlineModel, position: IPosition): (OutlineModel | OutlineGroup | OutlineElement)[] { + if (!model) { + return []; + } + let item: OutlineGroup | OutlineElement = model.getItemEnclosingPosition(position); + if (!item) { + return [model]; + } + let chain: (OutlineGroup | OutlineElement)[] = []; + while (item) { + chain.push(item); + let parent = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && size(parent.parent.children) === 1) { + break; + } + item = parent; + } + return chain.reverse(); + } + + private _updateOutlineElements(elements: (OutlineModel | OutlineGroup | OutlineElement)[]): void { + if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { + this._outlineElements = elements; + this._onDidUpdate.fire(this); + } + } + + private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | OutlineGroup | OutlineElement): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.id === b.id; + } + } +} diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts new file mode 100644 index 00000000000..2207e488347 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -0,0 +1,484 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; +import * as dom from 'vs/base/browser/dom'; +import { compareFileNames } from 'vs/base/common/comparers'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { createMatches, FuzzyScore, fuzzyScore } from 'vs/base/common/filters'; +import * as glob from 'vs/base/common/glob'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { join } from 'vs/base/common/paths'; +import { basename, dirname, isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; +import 'vs/css!./media/breadcrumbscontrol'; +import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IConstructorSignature1, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { HighlightingWorkbenchTree, IHighlighter, IHighlightingTreeConfiguration, IHighlightingTreeOptions } from 'vs/platform/list/browser/listService'; +import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { FileLabel } from 'vs/workbench/browser/labels'; +import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; + +export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { + let ctor: IConstructorSignature1 = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker; + return instantiationService.createInstance(ctor, parent); +} + +interface ILayoutInfo { + // height: number; + width: number; + arrowSize: number; + arrowOffset: number; + inputHeight: number; +} + +export abstract class BreadcrumbsPicker { + + protected readonly _disposables = new Array(); + protected readonly _domNode: HTMLDivElement; + protected readonly _arrow: HTMLDivElement; + protected readonly _treeContainer: HTMLDivElement; + protected readonly _tree: HighlightingWorkbenchTree; + protected readonly _focus: dom.IFocusTracker; + private _layoutInfo: ILayoutInfo; + + private readonly _onDidPickElement = new Emitter<{ target: any, payload: any }>(); + readonly onDidPickElement: Event<{ target: any, payload: any }> = this._onDidPickElement.event; + + private readonly _onDidFocusElement = new Emitter<{ target: any, payload: any }>(); + readonly onDidFocusElement: Event<{ target: any, payload: any }> = this._onDidFocusElement.event; + + constructor( + parent: HTMLElement, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IWorkbenchThemeService protected readonly _themeService: IWorkbenchThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + this._domNode = document.createElement('div'); + this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; + parent.appendChild(this._domNode); + + this._focus = dom.trackFocus(this._domNode); + this._focus.onDidBlur(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }), undefined, this._disposables); + this._disposables.push(onDidChangeZoomLevel(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }))); + + const theme = this._themeService.getTheme(); + const color = theme.getColor(breadcrumbsPickerBackground); + + this._arrow = document.createElement('div'); + this._arrow.style.width = '0'; + this._arrow.style.borderStyle = 'solid'; + this._arrow.style.borderWidth = '8px'; + this._arrow.style.borderColor = `transparent transparent ${color.toString()}`; + this._domNode.appendChild(this._arrow); + + this._treeContainer = document.createElement('div'); + this._treeContainer.style.background = color.toString(); + this._treeContainer.style.paddingTop = '2px'; + this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`; + this._domNode.appendChild(this._treeContainer); + + const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService); + this._disposables.push(filterConfig); + + const treeConifg = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined, highlighter: undefined }); + this._tree = this._instantiationService.createInstance( + HighlightingWorkbenchTree, + this._treeContainer, + treeConifg, + { useShadows: false, filterOnType: filterConfig.getValue(), showTwistie: false, twistiePixels: 12 }, + { placeholder: localize('placeholder', "Find") } + ); + this._disposables.push(this._tree.onDidChangeSelection(e => { + if (e.payload !== this._tree) { + const target = this._getTargetFromEvent(e.selection[0], e.payload); + if (target) { + setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click + this._onDidPickElement.fire({ target, payload: e.payload }); + }, 0); + } + } + })); + this._disposables.push(this._tree.onDidChangeFocus(e => { + const target = this._getTargetFromEvent(e.focus, e.payload); + if (target) { + this._onDidFocusElement.fire({ target, payload: e.payload }); + } + })); + this._disposables.push(this._tree.onDidStartFiltering(() => { + this._layoutInfo.inputHeight = 36; + this._layout(); + })); + this._disposables.push(this._tree.onDidExpandItem(() => { + this._layout(); + })); + this._disposables.push(this._tree.onDidCollapseItem(() => { + this._layout(); + })); + + // tree icon theme specials + dom.addClass(this._treeContainer, 'file-icon-themable-tree'); + dom.addClass(this._treeContainer, 'show-file-icons'); + const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => { + dom.toggleClass(this._treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons); + dom.toggleClass(this._treeContainer, 'hide-arrows', fileIconTheme.hidesExplorerArrows === true); + }; + this._disposables.push(_themeService.onDidFileIconThemeChange(onFileIconThemeChange)); + onFileIconThemeChange(_themeService.getFileIconTheme()); + + this._domNode.focus(); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this._focus.dispose(); + } + + setInput(input: any, height: number, width: number, arrowSize: number, arrowOffset: number): void { + let actualInput = this._getInput(input); + this._tree.setInput(actualInput).then(() => { + + this._layoutInfo = { width, arrowSize, arrowOffset, inputHeight: 0 }; + this._layout(); + + // use proper selection, reveal + let selection = this._getInitialSelection(this._tree, input); + if (selection) { + return this._tree.reveal(selection, .5).then(() => { + this._tree.setSelection([selection], this._tree); + this._tree.setFocus(selection); + this._tree.domFocus(); + }); + } else { + this._tree.focusFirst(); + this._tree.setSelection([this._tree.getFocus()], this._tree); + this._tree.domFocus(); + return Promise.resolve(null); + } + }, onUnexpectedError); + } + + private _layout(info: ILayoutInfo = this._layoutInfo): void { + + let count = 0; + let nav = this._tree.getNavigator(undefined, false); + while (nav.next() && count < 13) { count += 1; } + + let treeHeight = count * 22; + let totalHeight = treeHeight + 2 + info.arrowSize; + + this._domNode.style.height = `${totalHeight}px`; + this._domNode.style.width = `${info.width}px`; + this._arrow.style.borderWidth = `${info.arrowSize}px`; + this._arrow.style.marginLeft = `${info.arrowOffset}px`; + this._treeContainer.style.height = `${treeHeight}px`; + this._treeContainer.style.width = `${info.width}px`; + this._tree.layout(); + this._layoutInfo = info; + + } + + protected abstract _getInput(input: BreadcrumbElement): any; + protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any; + protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration; + protected abstract _getTargetFromEvent(element: any, payload: any): any | undefined; +} + +//#region - Files + +export class FileDataSource implements IDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + getId(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): string { + if (URI.isUri(element)) { + return element.toString(); + } else if (IWorkspace.isIWorkspace(element)) { + return element.id; + } else if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + return element.uri.toString(); + } else { + return element.resource.toString(); + } + } + + hasChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): boolean { + return URI.isUri(element) || IWorkspace.isIWorkspace(element) || IWorkspaceFolder.isIWorkspaceFolder(element) || element.isDirectory; + } + + getChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): TPromise { + if (IWorkspace.isIWorkspace(element)) { + return TPromise.as(element.folders).then(folders => { + for (let child of folders) { + this._parents.set(element, child); + } + return folders; + }); + } + let uri: URI; + if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + uri = element.uri; + } else if (URI.isUri(element)) { + uri = element; + } else { + uri = element.resource; + } + return this._fileService.resolveFile(uri).then(stat => { + for (let child of stat.children) { + this._parents.set(stat, child); + } + return stat.children; + }); + } + + getParent(tree: ITree, element: IWorkspace | URI | IWorkspaceFolder | IFileStat): TPromise { + return TPromise.as(this._parents.get(element)); + } +} + +export class FileFilter implements IFilter { + + private readonly _cachedExpressions = new Map(); + private readonly _disposables: IDisposable[] = []; + + constructor( + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IConfigurationService configService: IConfigurationService, + ) { + const config = BreadcrumbsConfig.FileExcludes.bindTo(configService); + const update = () => { + _workspaceService.getWorkspace().folders.forEach(folder => { + const excludesConfig = config.getValue({ resource: folder.uri }); + if (!excludesConfig) { + return; + } + // adjust patterns to be absolute in case they aren't + // free floating (**/) + const adjustedConfig: glob.IExpression = {}; + for (const pattern in excludesConfig) { + if (typeof excludesConfig[pattern] !== 'boolean') { + continue; + } + let patternAbs = pattern.indexOf('**/') !== 0 + ? join(folder.uri.path, pattern) + : pattern; + + adjustedConfig[patternAbs] = excludesConfig[pattern]; + } + this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig)); + }); + }; + update(); + this._disposables.push( + config, + config.onDidChange(update), + _workspaceService.onDidChangeWorkspaceFolders(update) + ); + } + + dispose(): void { + dispose(this._disposables); + } + + isVisible(tree: ITree, element: IWorkspaceFolder | IFileStat): boolean { + if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + // not a file + return true; + } + const folder = this._workspaceService.getWorkspaceFolder(element.resource); + if (!folder || !this._cachedExpressions.has(folder.uri.toString())) { + // no folder or no filer + return true; + } + + const expression = this._cachedExpressions.get(folder.uri.toString()); + return !expression(element.resource.path, basename(element.resource)); + } +} + +export class FileHighlighter implements IHighlighter { + getHighlightsStorageKey(element: IFileStat | IWorkspaceFolder): string { + return IWorkspaceFolder.isIWorkspaceFolder(element) ? element.uri.toString() : element.resource.toString(); + } + getHighlights(tree: ITree, element: IFileStat | IWorkspaceFolder, pattern: string): FuzzyScore { + return fuzzyScore(pattern, element.name, undefined, true); + } +} + +export class FileRenderer implements IRenderer { + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configService: IConfigurationService, + ) { } + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return 'FileStat'; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + return this._instantiationService.createInstance(FileLabel, container, { supportHighlights: true }); + } + + renderElement(tree: ITree, element: IFileStat | IWorkspaceFolder, templateId: string, templateData: FileLabel): void { + let fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + let resource: URI; + let fileKind: FileKind; + if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + resource = element.uri; + fileKind = FileKind.ROOT_FOLDER; + } else { + resource = element.resource; + fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE; + } + templateData.setFile(resource, { + fileKind, + hidePath: true, + fileDecorations: fileDecorations, + matches: createMatches((tree as HighlightingWorkbenchTree).getHighlighterScore(element)), + extraClasses: ['picker-item'] + }); + } + + disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void { + templateData.dispose(); + } +} + +export class FileSorter implements ISorter { + compare(tree: ITree, a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { + if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) { + return a.index - b.index; + } else { + if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { + // same type -> compare on names + return compareFileNames(a.name, b.name); + } else if ((a as IFileStat).isDirectory) { + return -1; + } else { + return 1; + } + } + } +} + +export class BreadcrumbsFilePicker extends BreadcrumbsPicker { + + constructor( + parent: HTMLElement, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkbenchThemeService themeService: IWorkbenchThemeService, + @IConfigurationService configService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + ) { + super(parent, instantiationService, themeService, configService); + } + + protected _getInput(input: BreadcrumbElement): any { + let { uri, kind } = (input as FileElement); + if (kind === FileKind.ROOT_FOLDER) { + return this._workspaceService.getWorkspace(); + } else { + return dirname(uri); + } + } + + protected _getInitialSelection(tree: ITree, input: BreadcrumbElement): any { + let { uri } = (input as FileElement); + let nav = tree.getNavigator(); + while (nav.next()) { + let cur = nav.current(); + let candidate = IWorkspaceFolder.isIWorkspaceFolder(cur) ? cur.uri : (cur as IFileStat).resource; + if (isEqual(uri, candidate)) { + return cur; + } + } + return undefined; + } + + protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { + // todo@joh reuse explorer implementations? + const filter = this._instantiationService.createInstance(FileFilter); + this._disposables.push(filter); + + config.dataSource = this._instantiationService.createInstance(FileDataSource); + config.renderer = this._instantiationService.createInstance(FileRenderer); + config.sorter = new FileSorter(); + config.highlighter = new FileHighlighter(); + config.filter = filter; + return config; + } + + protected _getTargetFromEvent(element: any, _payload: any): any | undefined { + if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { + return new FileElement((element as IFileStat).resource, FileKind.FILE); + } + } +} +//#endregion + +//#region - Symbols + +class OutlineHighlighter implements IHighlighter { + getHighlights(tree: ITree, element: OutlineElement, pattern: string): FuzzyScore { + OutlineModel.get(element).updateMatches(pattern); + return element.score; + } +} + +export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { + + protected _getInput(input: BreadcrumbElement): any { + let element = input as TreeElement; + let model = OutlineModel.get(element); + model.updateMatches(''); + return model; + } + + protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any { + return input instanceof OutlineModel ? undefined : input; + } + + protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { + config.dataSource = this._instantiationService.createInstance(OutlineDataSource); + config.renderer = this._instantiationService.createInstance(OutlineRenderer); + config.sorter = new OutlineItemComparator(); + config.highlighter = new OutlineHighlighter(); + return config; + } + + protected _getTargetFromEvent(element: any, payload: any): any | undefined { + if (payload && payload.didClickOnTwistie) { + return; + } + if (element instanceof OutlineElement) { + return element; + } + } +} + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fa58cd1702a..924fc705a8e 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -6,10 +6,11 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Action, IAction } from 'vs/base/common/actions'; import { IEditorQuickOpenEntry, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; -import { StatusbarItemDescriptor, StatusbarAlignment, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarItemDescriptor, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -37,13 +38,13 @@ import { ShowEditorsInActiveGroupAction, MoveEditorToLastGroupAction, OpenFirstEditorInGroup, MoveGroupUpAction, MoveGroupDownAction, FocusLastGroupAction, SplitEditorLeftAction, SplitEditorRightAction, SplitEditorUpAction, SplitEditorDownAction, MoveEditorToLeftGroupAction, MoveEditorToRightGroupAction, MoveEditorToAboveGroupAction, MoveEditorToBelowGroupAction, CloseAllEditorGroupsAction, JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, - EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoColumnsRightAction, EditorLayoutCenteredAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, - NewEditorGroupAboveAction, NewEditorGroupBelowAction + EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, + NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getQuickNavigateHandler, inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { AllEditorsPicker, ActiveEditorGroupPicker } from 'vs/workbench/browser/parts/editor/editorPicker'; @@ -109,10 +110,9 @@ class UntitledEditorInputFactory implements IEditorInputFactory { constructor( @ITextFileService private textFileService: ITextFileService - ) { - } + ) { } - public serialize(editorInput: EditorInput): string { + serialize(editorInput: EditorInput): string { if (!this.textFileService.isHotExitEnabled) { return null; // never restore untitled unless hot exit is enabled } @@ -121,7 +121,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { let resource = untitledEditorInput.getResource(); if (untitledEditorInput.hasAssociatedFilePath) { - resource = URI.file(resource.fsPath); // untitled with associated file path use the file schema + resource = resource.with({ scheme: Schemas.file }); // untitled with associated file path use the file schema } const serialized: ISerializedUntitledEditorInput = { @@ -134,7 +134,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { return JSON.stringify(serialized); } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledEditorInput { + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledEditorInput { return instantiationService.invokeFunction(accessor => { const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); @@ -163,7 +163,7 @@ interface ISerializedSideBySideEditorInput { // Register Side by Side Editor Input Factory class SideBySideEditorInputFactory implements IEditorInputFactory { - public serialize(editorInput: EditorInput): string { + serialize(editorInput: EditorInput): string { const input = editorInput; if (input.details && input.master) { @@ -191,7 +191,7 @@ class SideBySideEditorInputFactory implements IEditorInputFactory { return null; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { const deserialized: ISerializedSideBySideEditorInput = JSON.parse(serializedEditorInput); const registry = Registry.as(EditorInputExtensions.EditorInputFactories); @@ -230,13 +230,13 @@ export class QuickOpenActionContributor extends ActionBarContributor { super(); } - public hasActions(context: any): boolean { + hasActions(context: any): boolean { const entry = this.getEntry(context); return !!entry; } - public getActions(context: any): IAction[] { + getActions(context: any): IAction[] { const actions: Action[] = []; const entry = this.getEntry(context); @@ -319,6 +319,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllEditorGroupsAc registry.registerWorkbenchAction(new SyncActionDescriptor(CloseLeftEditorsInGroupAction, CloseLeftEditorsInGroupAction.ID, CloseLeftEditorsInGroupAction.LABEL), 'View: Close Editors in Group to the Left', category); registry.registerWorkbenchAction(new SyncActionDescriptor(CloseEditorsInOtherGroupsAction, CloseEditorsInOtherGroupsAction.ID, CloseEditorsInOtherGroupsAction.LABEL), 'View: Close Editors in Other Groups', category); registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH }), 'View: Split Editor', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorOrthogonalAction, SplitEditorOrthogonalAction.ID, SplitEditorOrthogonalAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_BACKSLASH) }), 'View: Split Editor Orthogonal', category); registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorLeftAction, SplitEditorLeftAction.ID, SplitEditorLeftAction.LABEL), 'View: Split Editor Left', category); registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorRightAction, SplitEditorRightAction.ID, SplitEditorRightAction.LABEL), 'View: Split Editor Right', category); registry.registerWorkbenchAction(new SyncActionDescriptor(SplitEditorUpAction, SplitEditorUpAction.ID, SplitEditorUpAction.LABEL), 'Split Editor Up', category); @@ -328,7 +329,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(JoinAllGroupsAction, J registry.registerWorkbenchAction(new SyncActionDescriptor(NavigateBetweenGroupsAction, NavigateBetweenGroupsAction.ID, NavigateBetweenGroupsAction.LABEL), 'View: Navigate Between Editor Groups', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ResetGroupSizesAction, ResetGroupSizesAction.ID, ResetGroupSizesAction.LABEL), 'View: Reset Editor Group Sizes', category); registry.registerWorkbenchAction(new SyncActionDescriptor(MaximizeGroupAction, MaximizeGroupAction.ID, MaximizeGroupAction.LABEL), 'View: Maximize Editor Group and Hide Sidebar', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(MinimizeOtherGroupsAction, MinimizeOtherGroupsAction.ID, MinimizeOtherGroupsAction.LABEL), 'View: Minimize Other Editor Groups', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(MinimizeOtherGroupsAction, MinimizeOtherGroupsAction.ID, MinimizeOtherGroupsAction.LABEL), 'View: Maximize Editor Group', category); registry.registerWorkbenchAction(new SyncActionDescriptor(MoveEditorLeftInGroupAction, MoveEditorLeftInGroupAction.ID, MoveEditorLeftInGroupAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.PageUp, mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow) } }), 'View: Move Editor Left', category); registry.registerWorkbenchAction(new SyncActionDescriptor(MoveEditorRightInGroupAction, MoveEditorRightInGroupAction.ID, MoveEditorRightInGroupAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.PageDown, mac: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow) } }), 'View: Move Editor Right', category); registry.registerWorkbenchAction(new SyncActionDescriptor(MoveGroupLeftAction, MoveGroupLeftAction.ID, MoveGroupLeftAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.LeftArrow) }), 'View: Move Editor Group Left', category); @@ -368,9 +369,8 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutThreeColum registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoRowsAction, EditorLayoutTwoRowsAction.ID, EditorLayoutTwoRowsAction.LABEL), 'View: Two Rows Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutThreeRowsAction, EditorLayoutThreeRowsAction.ID, EditorLayoutThreeRowsAction.LABEL), 'View: Three Rows Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoByTwoGridAction, EditorLayoutTwoByTwoGridAction.ID, EditorLayoutTwoByTwoGridAction.LABEL), 'View: Grid Editor Layout (2x2)', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoColumnsRightAction, EditorLayoutTwoColumnsRightAction.ID, EditorLayoutTwoColumnsRightAction.LABEL), 'View: Two Columns Right Editor Layout', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoRowsRightAction, EditorLayoutTwoRowsRightAction.ID, EditorLayoutTwoRowsRightAction.LABEL), 'View: Two Rows Right Editor Layout', category); registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoColumnsBottomAction.ID, EditorLayoutTwoColumnsBottomAction.LABEL), 'View: Two Columns Bottom Editor Layout', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(EditorLayoutCenteredAction, EditorLayoutCenteredAction.ID, EditorLayoutCenteredAction.LABEL), 'View: Centered Editor Layout', category); // Register Editor Picker Actions including quick navigate support const openNextEditorKeybinding = { primary: KeyMod.CtrlCmd | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyCode.Tab } }; @@ -381,7 +381,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousRecentlyUs const quickOpenNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigateNextInEditorPickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigateNextInEditorPickerId, true), when: editorPickerContext, primary: openNextEditorKeybinding.primary, @@ -391,7 +391,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const quickOpenNavigatePreviousInEditorPickerId = 'workbench.action.quickOpenNavigatePreviousInEditorPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigatePreviousInEditorPickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigatePreviousInEditorPickerId, false), when: editorPickerContext, primary: openPreviousEditorKeybinding.primary, @@ -404,12 +404,12 @@ editorCommands.setup(); // Touch Bar if (isMacintosh) { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { - command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, iconPath: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/back-tb.png')).fsPath } }, + command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/back-tb.png')) } }, group: 'navigation' }); MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { - command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, iconPath: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/forward-tb.png')).fsPath } }, + command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/browser/parts/editor/media/forward-tb.png')) } }, group: 'navigation' }); } @@ -446,17 +446,17 @@ function appendEditorToolItem(primary: IEditorToolItem, alternative: IEditorTool command: { id: primary.id, title: primary.title, - iconPath: { - dark: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${primary.iconDark}`)).fsPath, - light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${primary.iconLight}`)).fsPath + iconLocation: { + dark: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${primary.iconDark}`)), + light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${primary.iconLight}`)) } }, alt: { id: alternative.id, title: alternative.title, - iconPath: { - dark: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconDark}`)).fsPath, - light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconLight}`)).fsPath + iconLocation: { + dark: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconDark}`)), + light: URI.parse(require.toUrl(`vs/workbench/browser/parts/editor/media/${alternative.iconLight}`)) } }, group: 'navigation', @@ -503,8 +503,8 @@ appendEditorToolItem( { id: editorCommands.CLOSE_EDITOR_COMMAND_ID, title: nls.localize('close', "Close"), - iconDark: 'close-editor-inverse.svg', - iconLight: 'close-editor.svg' + iconDark: 'close-big-inverse-alt.svg', + iconLight: 'close-big-alt.svg' }, { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All"), @@ -519,8 +519,8 @@ appendEditorToolItem( { id: editorCommands.CLOSE_EDITOR_COMMAND_ID, title: nls.localize('close', "Close"), - iconDark: 'close-dirty-inverse.svg', - iconLight: 'close-dirty.svg' + iconDark: 'close-dirty-inverse-alt.svg', + iconLight: 'close-dirty-alt.svg' }, { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All"), @@ -537,3 +537,312 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorComman MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeOtherEditors', "Close Other Editors in Group"), category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: nls.localize('closeRightEditors', "Close Editors to the Right in Group"), category } }); + +// File menu +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: '1_editor', + command: { + id: ReopenClosedEditorAction.ID, + title: nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'z_clear', + command: { + id: ClearRecentFilesAction.ID, + title: nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened") + }, + order: 1 +}); + +// Layout menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miEditorLayout', comment: ['&& denotes a mnemonic'] }, "Editor &&Layout"), + submenu: MenuId.MenubarLayoutMenu, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_UP, + title: nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_DOWN, + title: nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_LEFT, + title: nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: editorCommands.SPLIT_EDITOR_RIGHT, + title: nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutSingleAction.ID, + title: nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoColumnsAction.ID, + title: nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutThreeColumnsAction.ID, + title: nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoRowsAction.ID, + title: nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutThreeRowsAction.ID, + title: nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows") + }, + order: 6 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoByTwoGridAction.ID, + title: nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)") + }, + order: 7 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoRowsRightAction.ID, + title: nls.localize({ key: 'miTwoRowsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two R&&ows Right") + }, + order: 8 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: EditorLayoutTwoColumnsBottomAction.ID, + title: nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom") + }, + order: 9 +}); + +// Main Menu Bar Contributions: + +// Forward/Back +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '1_fwd_back', + command: { + id: 'workbench.action.navigateBack', + title: nls.localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back"), + precondition: ContextKeyExpr.has('canNavigateBack') + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '1_fwd_back', + command: { + id: 'workbench.action.navigateForward', + title: nls.localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward"), + precondition: ContextKeyExpr.has('canNavigateForward') + }, + order: 2 +}); + +// Switch Editor +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, { + group: '1_any', + command: { + id: 'workbench.action.nextEditor', + title: nls.localize({ key: 'miNextEditor', comment: ['&& denotes a mnemonic'] }, "&&Next Editor") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, { + group: '1_any', + command: { + id: 'workbench.action.previousEditor', + title: nls.localize({ key: 'miPreviousEditor', comment: ['&& denotes a mnemonic'] }, "&&Previous Editor") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, { + group: '2_used', + command: { + id: 'workbench.action.openNextRecentlyUsedEditorInGroup', + title: nls.localize({ key: 'miNextEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Used Editor in Group") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchEditorMenu, { + group: '2_used', + command: { + id: 'workbench.action.openPreviousRecentlyUsedEditorInGroup', + title: nls.localize({ key: 'miPreviousEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Used Editor in Group") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch', + title: nls.localize({ key: 'miSwitchEditor', comment: ['&& denotes a mnemonic'] }, "Switch &&Editor"), + submenu: MenuId.MenubarSwitchEditorMenu, + order: 1 +}); + +// Switch Group +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '1_focus_index', + command: { + id: 'workbench.action.focusFirstEditorGroup', + title: nls.localize({ key: 'miFocusFirstGroup', comment: ['&& denotes a mnemonic'] }, "Group &&1") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '1_focus_index', + command: { + id: 'workbench.action.focusSecondEditorGroup', + title: nls.localize({ key: 'miFocusSecondGroup', comment: ['&& denotes a mnemonic'] }, "Group &&2") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '1_focus_index', + command: { + id: 'workbench.action.focusThirdEditorGroup', + title: nls.localize({ key: 'miFocusThirdGroup', comment: ['&& denotes a mnemonic'] }, "Group &&3") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '1_focus_index', + command: { + id: 'workbench.action.focusFourthEditorGroup', + title: nls.localize({ key: 'miFocusFourthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&4") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '1_focus_index', + command: { + id: 'workbench.action.focusFifthEditorGroup', + title: nls.localize({ key: 'miFocusFifthGroup', comment: ['&& denotes a mnemonic'] }, "Group &&5") + }, + order: 5 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '2_next_prev', + command: { + id: 'workbench.action.focusNextGroup', + title: nls.localize({ key: 'miNextGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Group") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '2_next_prev', + command: { + id: 'workbench.action.focusPreviousGroup', + title: nls.localize({ key: 'miPreviousGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Group") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '3_directional', + command: { + id: 'workbench.action.focusLeftGroup', + title: nls.localize({ key: 'miFocusLeftGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Left") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '3_directional', + command: { + id: 'workbench.action.focusRightGroup', + title: nls.localize({ key: 'miFocusRightGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Right") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '3_directional', + command: { + id: 'workbench.action.focusAboveGroup', + title: nls.localize({ key: 'miFocusAboveGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Above") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarSwitchGroupMenu, { + group: '3_directional', + command: { + id: 'workbench.action.focusBelowGroup', + title: nls.localize({ key: 'miFocusBelowGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Below") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch', + title: nls.localize({ key: 'miSwitchGroup', comment: ['&& denotes a mnemonic'] }, "Switch &&Group"), + submenu: MenuId.MenubarSwitchGroupMenu, + order: 2 +}); \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 74518dd76c1..248fa7871f5 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -21,8 +21,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic export const EDITOR_TITLE_HEIGHT = 35; -export const EDITOR_MIN_DIMENSIONS = new Dimension(220, 70); -export const EDITOR_MAX_DIMENSIONS = new Dimension(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); +export const DEFAULT_EDITOR_MIN_DIMENSIONS = new Dimension(220, 70); +export const DEFAULT_EDITOR_MAX_DIMENSIONS = new Dimension(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); export interface IEditorPartOptions extends IWorkbenchEditorPartConfiguration { iconTheme?: string; @@ -117,6 +117,7 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito isEmpty(): boolean; setActive(isActive: boolean): void; setLabel(label: string): void; + relayout(): void; shutdown(): void; } @@ -159,4 +160,4 @@ export interface EditorGroupsServiceImpl extends IEditorGroupsService { * A promise that resolves when groups have been restored. */ readonly whenRestored: TPromise; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 5b97aceda66..5c579fa655d 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -37,32 +37,32 @@ export class ExecuteCommandAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.commandService.executeCommand(this.commandId, this.commandArgs); } } -export class SplitEditorAction extends Action { - - public static readonly ID = 'workbench.action.splitEditor'; - public static readonly LABEL = nls.localize('splitEditor', "Split Editor"); - +export class BaseSplitEditorAction extends Action { private toDispose: IDisposable[] = []; private direction: GroupDirection; constructor( id: string, label: string, - @IEditorGroupsService private editorGroupService: IEditorGroupsService, - @IConfigurationService private configurationService: IConfigurationService + protected editorGroupService: IEditorGroupsService, + protected configurationService: IConfigurationService ) { super(id, label); - this.direction = preferredSideBySideGroupDirection(configurationService); + this.direction = this.getDirection(); this.registerListeners(); } + protected getDirection(): GroupDirection { + return preferredSideBySideGroupDirection(this.configurationService); + } + private registerListeners(): void { this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('workbench.editor.openSideBySideDirection')) { @@ -71,23 +71,59 @@ export class SplitEditorAction extends Action { })); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { splitEditor(this.editorGroupService, this.direction, context); return TPromise.as(true); } - public dispose(): void { + dispose(): void { super.dispose(); this.toDispose = dispose(this.toDispose); } } +export class SplitEditorAction extends BaseSplitEditorAction { + + static readonly ID = 'workbench.action.splitEditor'; + static readonly LABEL = nls.localize('splitEditor', "Split Editor"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(id, label, editorGroupService, configurationService); + } +} + +export class SplitEditorOrthogonalAction extends BaseSplitEditorAction { + + static readonly ID = 'workbench.action.splitEditorOrthogonal'; + static readonly LABEL = nls.localize('splitEditorOrthogonal', "Split Editor Orthogonal"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(id, label, editorGroupService, configurationService); + } + + protected getDirection(): GroupDirection { + const direction = preferredSideBySideGroupDirection(this.configurationService); + + return direction === GroupDirection.RIGHT ? GroupDirection.DOWN : GroupDirection.RIGHT; + } +} + export class SplitEditorLeftAction extends ExecuteCommandAction { - public static readonly ID = SPLIT_EDITOR_LEFT; - public static readonly LABEL = nls.localize('splitEditorGroupLeft', "Split Editor Left"); + static readonly ID = SPLIT_EDITOR_LEFT; + static readonly LABEL = nls.localize('splitEditorGroupLeft', "Split Editor Left"); constructor( id: string, @@ -100,8 +136,8 @@ export class SplitEditorLeftAction extends ExecuteCommandAction { export class SplitEditorRightAction extends ExecuteCommandAction { - public static readonly ID = SPLIT_EDITOR_RIGHT; - public static readonly LABEL = nls.localize('splitEditorGroupRight', "Split Editor Right"); + static readonly ID = SPLIT_EDITOR_RIGHT; + static readonly LABEL = nls.localize('splitEditorGroupRight', "Split Editor Right"); constructor( id: string, @@ -114,8 +150,8 @@ export class SplitEditorRightAction extends ExecuteCommandAction { export class SplitEditorUpAction extends ExecuteCommandAction { - public static readonly ID = SPLIT_EDITOR_UP; - public static readonly LABEL = nls.localize('splitEditorGroupUp', "Split Editor Up"); + static readonly ID = SPLIT_EDITOR_UP; + static readonly LABEL = nls.localize('splitEditorGroupUp', "Split Editor Up"); constructor( id: string, @@ -128,8 +164,8 @@ export class SplitEditorUpAction extends ExecuteCommandAction { export class SplitEditorDownAction extends ExecuteCommandAction { - public static readonly ID = SPLIT_EDITOR_DOWN; - public static readonly LABEL = nls.localize('splitEditorGroupDown', "Split Editor Down"); + static readonly ID = SPLIT_EDITOR_DOWN; + static readonly LABEL = nls.localize('splitEditorGroupDown', "Split Editor Down"); constructor( id: string, @@ -142,8 +178,8 @@ export class SplitEditorDownAction extends ExecuteCommandAction { export class JoinTwoGroupsAction extends Action { - public static readonly ID = 'workbench.action.joinTwoGroups'; - public static readonly LABEL = nls.localize('joinTwoGroups', "Join Editor Group with Next Group"); + static readonly ID = 'workbench.action.joinTwoGroups'; + static readonly LABEL = nls.localize('joinTwoGroups', "Join Editor Group with Next Group"); constructor( id: string, @@ -153,7 +189,7 @@ export class JoinTwoGroupsAction extends Action { super(id, label); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { let sourceGroup: IEditorGroup; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -177,8 +213,8 @@ export class JoinTwoGroupsAction extends Action { export class JoinAllGroupsAction extends Action { - public static readonly ID = 'workbench.action.joinAllGroups'; - public static readonly LABEL = nls.localize('joinAllGroups', "Join All Editor Groups"); + static readonly ID = 'workbench.action.joinAllGroups'; + static readonly LABEL = nls.localize('joinAllGroups', "Join All Editor Groups"); constructor( id: string, @@ -188,7 +224,7 @@ export class JoinAllGroupsAction extends Action { super(id, label); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { mergeAllGroups(this.editorGroupService); return TPromise.as(true); @@ -197,8 +233,8 @@ export class JoinAllGroupsAction extends Action { export class NavigateBetweenGroupsAction extends Action { - public static readonly ID = 'workbench.action.navigateEditorGroups'; - public static readonly LABEL = nls.localize('navigateEditorGroups', "Navigate Between Editor Groups"); + static readonly ID = 'workbench.action.navigateEditorGroups'; + static readonly LABEL = nls.localize('navigateEditorGroups', "Navigate Between Editor Groups"); constructor( id: string, @@ -208,7 +244,7 @@ export class NavigateBetweenGroupsAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const nextGroup = this.editorGroupService.findGroup({ location: GroupLocation.NEXT }, this.editorGroupService.activeGroup, true); nextGroup.focus(); @@ -218,8 +254,8 @@ export class NavigateBetweenGroupsAction extends Action { export class FocusActiveGroupAction extends Action { - public static readonly ID = 'workbench.action.focusActiveEditorGroup'; - public static readonly LABEL = nls.localize('focusActiveEditorGroup', "Focus Active Editor Group"); + static readonly ID = 'workbench.action.focusActiveEditorGroup'; + static readonly LABEL = nls.localize('focusActiveEditorGroup', "Focus Active Editor Group"); constructor( id: string, @@ -229,7 +265,7 @@ export class FocusActiveGroupAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { this.editorGroupService.activeGroup.focus(); return TPromise.as(true); @@ -247,7 +283,7 @@ export abstract class BaseFocusGroupAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const group = this.editorGroupService.findGroup(this.scope, this.editorGroupService.activeGroup, true); if (group) { group.focus(); @@ -259,8 +295,8 @@ export abstract class BaseFocusGroupAction extends Action { export class FocusFirstGroupAction extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusFirstEditorGroup'; - public static readonly LABEL = nls.localize('focusFirstEditorGroup', "Focus First Editor Group"); + static readonly ID = 'workbench.action.focusFirstEditorGroup'; + static readonly LABEL = nls.localize('focusFirstEditorGroup', "Focus First Editor Group"); constructor( id: string, @@ -273,8 +309,8 @@ export class FocusFirstGroupAction extends BaseFocusGroupAction { export class FocusLastGroupAction extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusLastEditorGroup'; - public static readonly LABEL = nls.localize('focusLastEditorGroup', "Focus Last Editor Group"); + static readonly ID = 'workbench.action.focusLastEditorGroup'; + static readonly LABEL = nls.localize('focusLastEditorGroup', "Focus Last Editor Group"); constructor( id: string, @@ -287,8 +323,8 @@ export class FocusLastGroupAction extends BaseFocusGroupAction { export class FocusNextGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusNextGroup'; - public static readonly LABEL = nls.localize('focusNextGroup', "Focus Next Editor Group"); + static readonly ID = 'workbench.action.focusNextGroup'; + static readonly LABEL = nls.localize('focusNextGroup', "Focus Next Editor Group"); constructor( id: string, @@ -301,8 +337,8 @@ export class FocusNextGroup extends BaseFocusGroupAction { export class FocusPreviousGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusPreviousGroup'; - public static readonly LABEL = nls.localize('focusPreviousGroup', "Focus Previous Editor Group"); + static readonly ID = 'workbench.action.focusPreviousGroup'; + static readonly LABEL = nls.localize('focusPreviousGroup', "Focus Previous Editor Group"); constructor( id: string, @@ -315,8 +351,8 @@ export class FocusPreviousGroup extends BaseFocusGroupAction { export class FocusLeftGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusLeftGroup'; - public static readonly LABEL = nls.localize('focusLeftGroup', "Focus Left Editor Group"); + static readonly ID = 'workbench.action.focusLeftGroup'; + static readonly LABEL = nls.localize('focusLeftGroup', "Focus Left Editor Group"); constructor( id: string, @@ -329,8 +365,8 @@ export class FocusLeftGroup extends BaseFocusGroupAction { export class FocusRightGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusRightGroup'; - public static readonly LABEL = nls.localize('focusRightGroup', "Focus Right Editor Group"); + static readonly ID = 'workbench.action.focusRightGroup'; + static readonly LABEL = nls.localize('focusRightGroup', "Focus Right Editor Group"); constructor( id: string, @@ -343,8 +379,8 @@ export class FocusRightGroup extends BaseFocusGroupAction { export class FocusAboveGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusAboveGroup'; - public static readonly LABEL = nls.localize('focusAboveGroup', "Focus Above Editor Group"); + static readonly ID = 'workbench.action.focusAboveGroup'; + static readonly LABEL = nls.localize('focusAboveGroup', "Focus Above Editor Group"); constructor( id: string, @@ -357,8 +393,8 @@ export class FocusAboveGroup extends BaseFocusGroupAction { export class FocusBelowGroup extends BaseFocusGroupAction { - public static readonly ID = 'workbench.action.focusBelowGroup'; - public static readonly LABEL = nls.localize('focusBelowGroup', "Focus Below Editor Group"); + static readonly ID = 'workbench.action.focusBelowGroup'; + static readonly LABEL = nls.localize('focusBelowGroup', "Focus Below Editor Group"); constructor( id: string, @@ -371,8 +407,8 @@ export class FocusBelowGroup extends BaseFocusGroupAction { export class OpenToSideFromQuickOpenAction extends Action { - public static readonly OPEN_TO_SIDE_ID = 'workbench.action.openToSide'; - public static readonly OPEN_TO_SIDE_LABEL = nls.localize('openToSide', "Open to the Side"); + static readonly OPEN_TO_SIDE_ID = 'workbench.action.openToSide'; + static readonly OPEN_TO_SIDE_LABEL = nls.localize('openToSide', "Open to the Side"); constructor( @IEditorService private editorService: IEditorService, @@ -383,13 +419,13 @@ export class OpenToSideFromQuickOpenAction extends Action { this.updateClass(); } - public updateClass(): void { + updateClass(): void { const preferredDirection = preferredSideBySideGroupDirection(this.configurationService); this.class = (preferredDirection === GroupDirection.RIGHT) ? 'quick-open-sidebyside-vertical' : 'quick-open-sidebyside-horizontal'; } - public run(context: any): TPromise { + run(context: any): TPromise { const entry = toEditorQuickOpenEntry(context); if (entry) { const input = entry.getInput(); @@ -427,8 +463,8 @@ export function toEditorQuickOpenEntry(element: any): IEditorQuickOpenEntry { export class CloseEditorAction extends Action { - public static readonly ID = 'workbench.action.closeActiveEditor'; - public static readonly LABEL = nls.localize('closeEditor', "Close Editor"); + static readonly ID = 'workbench.action.closeActiveEditor'; + static readonly LABEL = nls.localize('closeEditor', "Close Editor"); constructor( id: string, @@ -438,15 +474,15 @@ export class CloseEditorAction extends Action { super(id, label, 'close-editor-action'); } - public run(context?: IEditorCommandsContext): TPromise { + run(context?: IEditorCommandsContext): TPromise { return this.commandService.executeCommand(CLOSE_EDITOR_COMMAND_ID, void 0, context); } } export class CloseOneEditorAction extends Action { - public static readonly ID = 'workbench.action.closeActiveEditor'; - public static readonly LABEL = nls.localize('closeOneEditor', "Close"); + static readonly ID = 'workbench.action.closeActiveEditor'; + static readonly LABEL = nls.localize('closeOneEditor', "Close"); constructor( id: string, @@ -456,7 +492,7 @@ export class CloseOneEditorAction extends Action { super(id, label, 'close-editor-action'); } - public run(context?: IEditorCommandsContext): TPromise { + run(context?: IEditorCommandsContext): TPromise { let group: IEditorGroup; let editorIndex: number; if (context) { @@ -490,8 +526,8 @@ export class CloseOneEditorAction extends Action { export class RevertAndCloseEditorAction extends Action { - public static readonly ID = 'workbench.action.revertAndCloseActiveEditor'; - public static readonly LABEL = nls.localize('revertAndCloseActiveEditor', "Revert and Close Editor"); + static readonly ID = 'workbench.action.revertAndCloseActiveEditor'; + static readonly LABEL = nls.localize('revertAndCloseActiveEditor', "Revert and Close Editor"); constructor( id: string, @@ -501,7 +537,7 @@ export class RevertAndCloseEditorAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const activeControl = this.editorService.activeControl; if (activeControl) { const editor = activeControl.input; @@ -523,8 +559,8 @@ export class RevertAndCloseEditorAction extends Action { export class CloseLeftEditorsInGroupAction extends Action { - public static readonly ID = 'workbench.action.closeEditorsToTheLeft'; - public static readonly LABEL = nls.localize('closeEditorsToTheLeft', "Close Editors to the Left in Group"); + static readonly ID = 'workbench.action.closeEditorsToTheLeft'; + static readonly LABEL = nls.localize('closeEditorsToTheLeft', "Close Editors to the Left in Group"); constructor( id: string, @@ -535,7 +571,7 @@ export class CloseLeftEditorsInGroupAction extends Action { super(id, label); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { const { group, editor } = getTarget(this.editorService, this.editorGroupService, context); if (group && editor) { return group.closeEditors({ direction: CloseDirection.LEFT, except: editor }); @@ -580,7 +616,7 @@ export abstract class BaseCloseAllAction extends Action { return groupsToClose; } - public run(): TPromise { + run(): TPromise { // Just close all if there are no or one dirty editor if (this.textFileService.getDirty().length < 2) { @@ -615,8 +651,8 @@ export abstract class BaseCloseAllAction extends Action { export class CloseAllEditorsAction extends BaseCloseAllAction { - public static readonly ID = 'workbench.action.closeAllEditors'; - public static readonly LABEL = nls.localize('closeAllEditors', "Close All Editors"); + static readonly ID = 'workbench.action.closeAllEditors'; + static readonly LABEL = nls.localize('closeAllEditors', "Close All Editors"); constructor( id: string, @@ -634,8 +670,8 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { export class CloseAllEditorGroupsAction extends BaseCloseAllAction { - public static readonly ID = 'workbench.action.closeAllGroups'; - public static readonly LABEL = nls.localize('closeAllGroups', "Close All Editor Groups"); + static readonly ID = 'workbench.action.closeAllGroups'; + static readonly LABEL = nls.localize('closeAllGroups', "Close All Editor Groups"); constructor( id: string, @@ -655,8 +691,8 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { export class CloseEditorsInOtherGroupsAction extends Action { - public static readonly ID = 'workbench.action.closeEditorsInOtherGroups'; - public static readonly LABEL = nls.localize('closeEditorsInOtherGroups', "Close Editors in Other Groups"); + static readonly ID = 'workbench.action.closeEditorsInOtherGroups'; + static readonly LABEL = nls.localize('closeEditorsInOtherGroups', "Close Editors in Other Groups"); constructor( id: string, @@ -666,7 +702,7 @@ export class CloseEditorsInOtherGroupsAction extends Action { super(id, label); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; return TPromise.join(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => { if (g.id === groupToSkip.id) { @@ -689,7 +725,7 @@ export class BaseMoveGroupAction extends Action { super(id, label); } - public run(context?: IEditorIdentifier): TPromise { + run(context?: IEditorIdentifier): TPromise { let sourceGroup: IEditorGroup; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -735,8 +771,8 @@ export class BaseMoveGroupAction extends Action { export class MoveGroupLeftAction extends BaseMoveGroupAction { - public static readonly ID = 'workbench.action.moveActiveEditorGroupLeft'; - public static readonly LABEL = nls.localize('moveActiveGroupLeft', "Move Editor Group Left"); + static readonly ID = 'workbench.action.moveActiveEditorGroupLeft'; + static readonly LABEL = nls.localize('moveActiveGroupLeft', "Move Editor Group Left"); constructor( id: string, @@ -749,8 +785,8 @@ export class MoveGroupLeftAction extends BaseMoveGroupAction { export class MoveGroupRightAction extends BaseMoveGroupAction { - public static readonly ID = 'workbench.action.moveActiveEditorGroupRight'; - public static readonly LABEL = nls.localize('moveActiveGroupRight', "Move Editor Group Right"); + static readonly ID = 'workbench.action.moveActiveEditorGroupRight'; + static readonly LABEL = nls.localize('moveActiveGroupRight', "Move Editor Group Right"); constructor( id: string, @@ -763,8 +799,8 @@ export class MoveGroupRightAction extends BaseMoveGroupAction { export class MoveGroupUpAction extends BaseMoveGroupAction { - public static readonly ID = 'workbench.action.moveActiveEditorGroupUp'; - public static readonly LABEL = nls.localize('moveActiveGroupUp', "Move Editor Group Up"); + static readonly ID = 'workbench.action.moveActiveEditorGroupUp'; + static readonly LABEL = nls.localize('moveActiveGroupUp', "Move Editor Group Up"); constructor( id: string, @@ -777,8 +813,8 @@ export class MoveGroupUpAction extends BaseMoveGroupAction { export class MoveGroupDownAction extends BaseMoveGroupAction { - public static readonly ID = 'workbench.action.moveActiveEditorGroupDown'; - public static readonly LABEL = nls.localize('moveActiveGroupDown', "Move Editor Group Down"); + static readonly ID = 'workbench.action.moveActiveEditorGroupDown'; + static readonly LABEL = nls.localize('moveActiveGroupDown', "Move Editor Group Down"); constructor( id: string, @@ -791,14 +827,14 @@ export class MoveGroupDownAction extends BaseMoveGroupAction { export class MinimizeOtherGroupsAction extends Action { - public static readonly ID = 'workbench.action.minimizeOtherEditors'; - public static readonly LABEL = nls.localize('minimizeOtherEditorGroups', "Minimize Other Editor Groups"); + static readonly ID = 'workbench.action.minimizeOtherEditors'; + static readonly LABEL = nls.localize('minimizeOtherEditorGroups', "Maximize Editor Group"); constructor(id: string, label: string, @IEditorGroupsService private editorGroupService: IEditorGroupsService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); return TPromise.as(false); @@ -807,14 +843,14 @@ export class MinimizeOtherGroupsAction extends Action { export class ResetGroupSizesAction extends Action { - public static readonly ID = 'workbench.action.evenEditorWidths'; - public static readonly LABEL = nls.localize('evenEditorGroups', "Reset Editor Group Sizes"); + static readonly ID = 'workbench.action.evenEditorWidths'; + static readonly LABEL = nls.localize('evenEditorGroups', "Reset Editor Group Sizes"); constructor(id: string, label: string, @IEditorGroupsService private editorGroupService: IEditorGroupsService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.editorGroupService.arrangeGroups(GroupsArrangement.EVEN); return TPromise.as(false); @@ -823,8 +859,8 @@ export class ResetGroupSizesAction extends Action { export class MaximizeGroupAction extends Action { - public static readonly ID = 'workbench.action.maximizeEditor'; - public static readonly LABEL = nls.localize('maximizeEditor', "Maximize Editor Group and Hide Sidebar"); + static readonly ID = 'workbench.action.maximizeEditor'; + static readonly LABEL = nls.localize('maximizeEditor', "Maximize Editor Group and Hide Sidebar"); constructor( id: string, @@ -836,7 +872,7 @@ export class MaximizeGroupAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { if (this.editorService.activeEditor) { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); @@ -858,7 +894,7 @@ export abstract class BaseNavigateEditorAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const result = this.navigate(); if (!result) { return TPromise.as(false); @@ -878,8 +914,8 @@ export abstract class BaseNavigateEditorAction extends Action { export class OpenNextEditor extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.nextEditor'; - public static readonly LABEL = nls.localize('openNextEditor', "Open Next Editor"); + static readonly ID = 'workbench.action.nextEditor'; + static readonly LABEL = nls.localize('openNextEditor', "Open Next Editor"); constructor( id: string, @@ -913,8 +949,8 @@ export class OpenNextEditor extends BaseNavigateEditorAction { export class OpenPreviousEditor extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.previousEditor'; - public static readonly LABEL = nls.localize('openPreviousEditor', "Open Previous Editor"); + static readonly ID = 'workbench.action.previousEditor'; + static readonly LABEL = nls.localize('openPreviousEditor', "Open Previous Editor"); constructor( id: string, @@ -948,8 +984,8 @@ export class OpenPreviousEditor extends BaseNavigateEditorAction { export class OpenNextEditorInGroup extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.nextEditorInGroup'; - public static readonly LABEL = nls.localize('nextEditorInGroup', "Open Next Editor in Group"); + static readonly ID = 'workbench.action.nextEditorInGroup'; + static readonly LABEL = nls.localize('nextEditorInGroup', "Open Next Editor in Group"); constructor( id: string, @@ -971,8 +1007,8 @@ export class OpenNextEditorInGroup extends BaseNavigateEditorAction { export class OpenPreviousEditorInGroup extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.previousEditorInGroup'; - public static readonly LABEL = nls.localize('openPreviousEditorInGroup', "Open Previous Editor in Group"); + static readonly ID = 'workbench.action.previousEditorInGroup'; + static readonly LABEL = nls.localize('openPreviousEditorInGroup', "Open Previous Editor in Group"); constructor( id: string, @@ -994,8 +1030,8 @@ export class OpenPreviousEditorInGroup extends BaseNavigateEditorAction { export class OpenFirstEditorInGroup extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.firstEditorInGroup'; - public static readonly LABEL = nls.localize('firstEditorInGroup', "Open First Editor in Group"); + static readonly ID = 'workbench.action.firstEditorInGroup'; + static readonly LABEL = nls.localize('firstEditorInGroup', "Open First Editor in Group"); constructor( id: string, @@ -1016,8 +1052,8 @@ export class OpenFirstEditorInGroup extends BaseNavigateEditorAction { export class OpenLastEditorInGroup extends BaseNavigateEditorAction { - public static readonly ID = 'workbench.action.lastEditorInGroup'; - public static readonly LABEL = nls.localize('lastEditorInGroup', "Open Last Editor in Group"); + static readonly ID = 'workbench.action.lastEditorInGroup'; + static readonly LABEL = nls.localize('lastEditorInGroup', "Open Last Editor in Group"); constructor( id: string, @@ -1038,14 +1074,14 @@ export class OpenLastEditorInGroup extends BaseNavigateEditorAction { export class NavigateForwardAction extends Action { - public static readonly ID = 'workbench.action.navigateForward'; - public static readonly LABEL = nls.localize('navigateNext', "Go Forward"); + static readonly ID = 'workbench.action.navigateForward'; + static readonly LABEL = nls.localize('navigateNext', "Go Forward"); constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.forward(); return TPromise.as(null); @@ -1054,14 +1090,14 @@ export class NavigateForwardAction extends Action { export class NavigateBackwardsAction extends Action { - public static readonly ID = 'workbench.action.navigateBack'; - public static readonly LABEL = nls.localize('navigatePrevious', "Go Back"); + static readonly ID = 'workbench.action.navigateBack'; + static readonly LABEL = nls.localize('navigatePrevious', "Go Back"); constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.back(); return TPromise.as(null); @@ -1070,14 +1106,14 @@ export class NavigateBackwardsAction extends Action { export class NavigateLastAction extends Action { - public static readonly ID = 'workbench.action.navigateLast'; - public static readonly LABEL = nls.localize('navigateLast', "Go Last"); + static readonly ID = 'workbench.action.navigateLast'; + static readonly LABEL = nls.localize('navigateLast', "Go Last"); constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.last(); return TPromise.as(null); @@ -1086,8 +1122,8 @@ export class NavigateLastAction extends Action { export class ReopenClosedEditorAction extends Action { - public static readonly ID = 'workbench.action.reopenClosedEditor'; - public static readonly LABEL = nls.localize('reopenClosedEditor', "Reopen Closed Editor"); + static readonly ID = 'workbench.action.reopenClosedEditor'; + static readonly LABEL = nls.localize('reopenClosedEditor', "Reopen Closed Editor"); constructor( id: string, @@ -1097,7 +1133,7 @@ export class ReopenClosedEditorAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.reopenLastClosedEditor(); return TPromise.as(false); @@ -1106,8 +1142,8 @@ export class ReopenClosedEditorAction extends Action { export class ClearRecentFilesAction extends Action { - public static readonly ID = 'workbench.action.clearRecentFiles'; - public static readonly LABEL = nls.localize('clearRecentFiles', "Clear Recently Opened"); + static readonly ID = 'workbench.action.clearRecentFiles'; + static readonly LABEL = nls.localize('clearRecentFiles', "Clear Recently Opened"); constructor( id: string, @@ -1117,7 +1153,7 @@ export class ClearRecentFilesAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { this.windowsService.clearRecentlyOpened(); return TPromise.as(false); @@ -1126,8 +1162,8 @@ export class ClearRecentFilesAction extends Action { export class ShowEditorsInActiveGroupAction extends QuickOpenAction { - public static readonly ID = 'workbench.action.showEditorsInActiveGroup'; - public static readonly LABEL = nls.localize('showEditorsInActiveGroup', "Show Editors in Active Group"); + static readonly ID = 'workbench.action.showEditorsInActiveGroup'; + static readonly LABEL = nls.localize('showEditorsInActiveGroup', "Show Editors in Active Group"); constructor( actionId: string, @@ -1140,8 +1176,8 @@ export class ShowEditorsInActiveGroupAction extends QuickOpenAction { export class ShowAllEditorsAction extends QuickOpenAction { - public static readonly ID = 'workbench.action.showAllEditors'; - public static readonly LABEL = nls.localize('showAllEditors', "Show All Editors"); + static readonly ID = 'workbench.action.showAllEditors'; + static readonly LABEL = nls.localize('showAllEditors', "Show All Editors"); constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) { super(actionId, actionLabel, NAVIGATE_ALL_EDITORS_GROUP_PREFIX, quickOpenService); @@ -1159,7 +1195,7 @@ export class BaseQuickOpenEditorInGroupAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const keys = this.keybindingService.lookupKeybindings(this.id); @@ -1172,8 +1208,8 @@ export class BaseQuickOpenEditorInGroupAction extends Action { export class OpenPreviousRecentlyUsedEditorInGroupAction extends BaseQuickOpenEditorInGroupAction { - public static readonly ID = 'workbench.action.openPreviousRecentlyUsedEditorInGroup'; - public static readonly LABEL = nls.localize('openPreviousRecentlyUsedEditorInGroup', "Open Previous Recently Used Editor in Group"); + static readonly ID = 'workbench.action.openPreviousRecentlyUsedEditorInGroup'; + static readonly LABEL = nls.localize('openPreviousRecentlyUsedEditorInGroup', "Open Previous Recently Used Editor in Group"); constructor( id: string, @@ -1187,8 +1223,8 @@ export class OpenPreviousRecentlyUsedEditorInGroupAction extends BaseQuickOpenEd export class OpenNextRecentlyUsedEditorInGroupAction extends BaseQuickOpenEditorInGroupAction { - public static readonly ID = 'workbench.action.openNextRecentlyUsedEditorInGroup'; - public static readonly LABEL = nls.localize('openNextRecentlyUsedEditorInGroup', "Open Next Recently Used Editor in Group"); + static readonly ID = 'workbench.action.openNextRecentlyUsedEditorInGroup'; + static readonly LABEL = nls.localize('openNextRecentlyUsedEditorInGroup', "Open Next Recently Used Editor in Group"); constructor( id: string, @@ -1202,8 +1238,8 @@ export class OpenNextRecentlyUsedEditorInGroupAction extends BaseQuickOpenEditor export class OpenPreviousEditorFromHistoryAction extends Action { - public static readonly ID = 'workbench.action.openPreviousEditorFromHistory'; - public static readonly LABEL = nls.localize('navigateEditorHistoryByInput', "Open Previous Editor from History"); + static readonly ID = 'workbench.action.openPreviousEditorFromHistory'; + static readonly LABEL = nls.localize('navigateEditorHistoryByInput', "Open Previous Editor from History"); constructor( id: string, @@ -1214,7 +1250,7 @@ export class OpenPreviousEditorFromHistoryAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const keys = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(null, { quickNavigateConfiguration: { keybindings: keys } }); @@ -1225,14 +1261,14 @@ export class OpenPreviousEditorFromHistoryAction extends Action { export class OpenNextRecentlyUsedEditorAction extends Action { - public static readonly ID = 'workbench.action.openNextRecentlyUsedEditor'; - public static readonly LABEL = nls.localize('openNextRecentlyUsedEditor', "Open Next Recently Used Editor"); + static readonly ID = 'workbench.action.openNextRecentlyUsedEditor'; + static readonly LABEL = nls.localize('openNextRecentlyUsedEditor', "Open Next Recently Used Editor"); constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.forward(true); return TPromise.as(null); @@ -1241,14 +1277,14 @@ export class OpenNextRecentlyUsedEditorAction extends Action { export class OpenPreviousRecentlyUsedEditorAction extends Action { - public static readonly ID = 'workbench.action.openPreviousRecentlyUsedEditor'; - public static readonly LABEL = nls.localize('openPreviousRecentlyUsedEditor', "Open Previous Recently Used Editor"); + static readonly ID = 'workbench.action.openPreviousRecentlyUsedEditor'; + static readonly LABEL = nls.localize('openPreviousRecentlyUsedEditor', "Open Previous Recently Used Editor"); constructor(id: string, label: string, @IHistoryService private historyService: IHistoryService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.historyService.back(true); return TPromise.as(null); @@ -1257,8 +1293,8 @@ export class OpenPreviousRecentlyUsedEditorAction extends Action { export class ClearEditorHistoryAction extends Action { - public static readonly ID = 'workbench.action.clearEditorHistory'; - public static readonly LABEL = nls.localize('clearEditorHistory', "Clear Editor History"); + static readonly ID = 'workbench.action.clearEditorHistory'; + static readonly LABEL = nls.localize('clearEditorHistory', "Clear Editor History"); constructor( id: string, @@ -1268,7 +1304,7 @@ export class ClearEditorHistoryAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { // Editor history this.historyService.clear(); @@ -1279,8 +1315,8 @@ export class ClearEditorHistoryAction extends Action { export class MoveEditorLeftInGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorLeftInGroup'; - public static readonly LABEL = nls.localize('moveEditorLeft', "Move Editor Left"); + static readonly ID = 'workbench.action.moveEditorLeftInGroup'; + static readonly LABEL = nls.localize('moveEditorLeft', "Move Editor Left"); constructor( id: string, @@ -1293,8 +1329,8 @@ export class MoveEditorLeftInGroupAction extends ExecuteCommandAction { export class MoveEditorRightInGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorRightInGroup'; - public static readonly LABEL = nls.localize('moveEditorRight', "Move Editor Right"); + static readonly ID = 'workbench.action.moveEditorRightInGroup'; + static readonly LABEL = nls.localize('moveEditorRight', "Move Editor Right"); constructor( id: string, @@ -1307,8 +1343,8 @@ export class MoveEditorRightInGroupAction extends ExecuteCommandAction { export class MoveEditorToPreviousGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToPreviousGroup'; - public static readonly LABEL = nls.localize('moveEditorToPreviousGroup', "Move Editor into Previous Group"); + static readonly ID = 'workbench.action.moveEditorToPreviousGroup'; + static readonly LABEL = nls.localize('moveEditorToPreviousGroup', "Move Editor into Previous Group"); constructor( id: string, @@ -1321,8 +1357,8 @@ export class MoveEditorToPreviousGroupAction extends ExecuteCommandAction { export class MoveEditorToNextGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToNextGroup'; - public static readonly LABEL = nls.localize('moveEditorToNextGroup', "Move Editor into Next Group"); + static readonly ID = 'workbench.action.moveEditorToNextGroup'; + static readonly LABEL = nls.localize('moveEditorToNextGroup', "Move Editor into Next Group"); constructor( id: string, @@ -1335,8 +1371,8 @@ export class MoveEditorToNextGroupAction extends ExecuteCommandAction { export class MoveEditorToAboveGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToAboveGroup'; - public static readonly LABEL = nls.localize('moveEditorToAboveGroup', "Move Editor into Above Group"); + static readonly ID = 'workbench.action.moveEditorToAboveGroup'; + static readonly LABEL = nls.localize('moveEditorToAboveGroup', "Move Editor into Above Group"); constructor( id: string, @@ -1349,8 +1385,8 @@ export class MoveEditorToAboveGroupAction extends ExecuteCommandAction { export class MoveEditorToBelowGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToBelowGroup'; - public static readonly LABEL = nls.localize('moveEditorToBelowGroup', "Move Editor into Below Group"); + static readonly ID = 'workbench.action.moveEditorToBelowGroup'; + static readonly LABEL = nls.localize('moveEditorToBelowGroup', "Move Editor into Below Group"); constructor( id: string, @@ -1363,8 +1399,8 @@ export class MoveEditorToBelowGroupAction extends ExecuteCommandAction { export class MoveEditorToLeftGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToLeftGroup'; - public static readonly LABEL = nls.localize('moveEditorToLeftGroup', "Move Editor into Left Group"); + static readonly ID = 'workbench.action.moveEditorToLeftGroup'; + static readonly LABEL = nls.localize('moveEditorToLeftGroup', "Move Editor into Left Group"); constructor( id: string, @@ -1377,8 +1413,8 @@ export class MoveEditorToLeftGroupAction extends ExecuteCommandAction { export class MoveEditorToRightGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToRightGroup'; - public static readonly LABEL = nls.localize('moveEditorToRightGroup', "Move Editor into Right Group"); + static readonly ID = 'workbench.action.moveEditorToRightGroup'; + static readonly LABEL = nls.localize('moveEditorToRightGroup', "Move Editor into Right Group"); constructor( id: string, @@ -1391,8 +1427,8 @@ export class MoveEditorToRightGroupAction extends ExecuteCommandAction { export class MoveEditorToFirstGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToFirstGroup'; - public static readonly LABEL = nls.localize('moveEditorToFirstGroup', "Move Editor into First Group"); + static readonly ID = 'workbench.action.moveEditorToFirstGroup'; + static readonly LABEL = nls.localize('moveEditorToFirstGroup', "Move Editor into First Group"); constructor( id: string, @@ -1405,8 +1441,8 @@ export class MoveEditorToFirstGroupAction extends ExecuteCommandAction { export class MoveEditorToLastGroupAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.moveEditorToLastGroup'; - public static readonly LABEL = nls.localize('moveEditorToLastGroup', "Move Editor into Last Group"); + static readonly ID = 'workbench.action.moveEditorToLastGroup'; + static readonly LABEL = nls.localize('moveEditorToLastGroup', "Move Editor into Last Group"); constructor( id: string, @@ -1419,8 +1455,8 @@ export class MoveEditorToLastGroupAction extends ExecuteCommandAction { export class EditorLayoutSingleAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutSingle'; - public static readonly LABEL = nls.localize('editorLayoutSingle', "Single Column Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutSingle'; + static readonly LABEL = nls.localize('editorLayoutSingle', "Single Column Editor Layout"); constructor( id: string, @@ -1433,8 +1469,8 @@ export class EditorLayoutSingleAction extends ExecuteCommandAction { export class EditorLayoutTwoColumnsAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutTwoColumns'; - public static readonly LABEL = nls.localize('editorLayoutTwoColumns', "Two Columns Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutTwoColumns'; + static readonly LABEL = nls.localize('editorLayoutTwoColumns', "Two Columns Editor Layout"); constructor( id: string, @@ -1447,8 +1483,8 @@ export class EditorLayoutTwoColumnsAction extends ExecuteCommandAction { export class EditorLayoutThreeColumnsAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutThreeColumns'; - public static readonly LABEL = nls.localize('editorLayoutThreeColumns', "Three Columns Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutThreeColumns'; + static readonly LABEL = nls.localize('editorLayoutThreeColumns', "Three Columns Editor Layout"); constructor( id: string, @@ -1461,8 +1497,8 @@ export class EditorLayoutThreeColumnsAction extends ExecuteCommandAction { export class EditorLayoutTwoRowsAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutTwoRows'; - public static readonly LABEL = nls.localize('editorLayoutTwoRows', "Two Rows Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutTwoRows'; + static readonly LABEL = nls.localize('editorLayoutTwoRows', "Two Rows Editor Layout"); constructor( id: string, @@ -1475,8 +1511,8 @@ export class EditorLayoutTwoRowsAction extends ExecuteCommandAction { export class EditorLayoutThreeRowsAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutThreeRows'; - public static readonly LABEL = nls.localize('editorLayoutThreeRows', "Three Rows Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutThreeRows'; + static readonly LABEL = nls.localize('editorLayoutThreeRows', "Three Rows Editor Layout"); constructor( id: string, @@ -1489,8 +1525,8 @@ export class EditorLayoutThreeRowsAction extends ExecuteCommandAction { export class EditorLayoutTwoByTwoGridAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutTwoByTwoGrid'; - public static readonly LABEL = nls.localize('editorLayoutTwoByTwoGrid', "Grid Editor Layout (2x2)"); + static readonly ID = 'workbench.action.editorLayoutTwoByTwoGrid'; + static readonly LABEL = nls.localize('editorLayoutTwoByTwoGrid', "Grid Editor Layout (2x2)"); constructor( id: string, @@ -1503,8 +1539,8 @@ export class EditorLayoutTwoByTwoGridAction extends ExecuteCommandAction { export class EditorLayoutTwoColumnsBottomAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutTwoColumnsBottom'; - public static readonly LABEL = nls.localize('editorLayoutTwoColumnsBottom', "Two Columns Bottom Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutTwoColumnsBottom'; + static readonly LABEL = nls.localize('editorLayoutTwoColumnsBottom', "Two Columns Bottom Editor Layout"); constructor( id: string, @@ -1515,10 +1551,10 @@ export class EditorLayoutTwoColumnsBottomAction extends ExecuteCommandAction { } } -export class EditorLayoutTwoColumnsRightAction extends ExecuteCommandAction { +export class EditorLayoutTwoRowsRightAction extends ExecuteCommandAction { - public static readonly ID = 'workbench.action.editorLayoutTwoColumnsRight'; - public static readonly LABEL = nls.localize('editorLayoutTwoColumnsRight', "Two Columns Right Editor Layout"); + static readonly ID = 'workbench.action.editorLayoutTwoRowsRight'; + static readonly LABEL = nls.localize('editorLayoutTwoRowsRight', "Two Rows Right Editor Layout"); constructor( id: string, @@ -1529,34 +1565,6 @@ export class EditorLayoutTwoColumnsRightAction extends ExecuteCommandAction { } } -export class EditorLayoutCenteredAction extends Action { - - public static readonly ID = 'workbench.action.editorLayoutCentered'; - public static readonly LABEL = nls.localize('editorLayoutCentered', "Centered Editor Layout"); - - constructor( - id: string, - label: string, - @IPartService private partService: IPartService, - @IEditorGroupsService private editorGroupService: IEditorGroupsService - ) { - super(id, label); - } - - public run(): TPromise { - - // Ensure we can enter centered editor layout even if there are more than 1 groups - if (this.editorGroupService.count > 1) { - mergeAllGroups(this.editorGroupService); - } - - // Center editor layout - this.partService.centerEditorLayout(true); - - return TPromise.as(true); - } -} - export class BaseCreateEditorGroupAction extends Action { constructor( @@ -1568,7 +1576,7 @@ export class BaseCreateEditorGroupAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { this.editorGroupService.addGroup(this.editorGroupService.activeGroup, this.direction, { activate: true }); return TPromise.as(true); @@ -1577,8 +1585,8 @@ export class BaseCreateEditorGroupAction extends Action { export class NewEditorGroupLeftAction extends BaseCreateEditorGroupAction { - public static readonly ID = 'workbench.action.newGroupLeft'; - public static readonly LABEL = nls.localize('newEditorLeft', "New Editor Group to the Left"); + static readonly ID = 'workbench.action.newGroupLeft'; + static readonly LABEL = nls.localize('newEditorLeft', "New Editor Group to the Left"); constructor( id: string, @@ -1591,8 +1599,8 @@ export class NewEditorGroupLeftAction extends BaseCreateEditorGroupAction { export class NewEditorGroupRightAction extends BaseCreateEditorGroupAction { - public static readonly ID = 'workbench.action.newGroupRight'; - public static readonly LABEL = nls.localize('newEditorRight', "New Editor Group to the Right"); + static readonly ID = 'workbench.action.newGroupRight'; + static readonly LABEL = nls.localize('newEditorRight', "New Editor Group to the Right"); constructor( id: string, @@ -1605,8 +1613,8 @@ export class NewEditorGroupRightAction extends BaseCreateEditorGroupAction { export class NewEditorGroupAboveAction extends BaseCreateEditorGroupAction { - public static readonly ID = 'workbench.action.newGroupAbove'; - public static readonly LABEL = nls.localize('newEditorAbove', "New Editor Group Above"); + static readonly ID = 'workbench.action.newGroupAbove'; + static readonly LABEL = nls.localize('newEditorAbove', "New Editor Group Above"); constructor( id: string, @@ -1619,8 +1627,8 @@ export class NewEditorGroupAboveAction extends BaseCreateEditorGroupAction { export class NewEditorGroupBelowAction extends BaseCreateEditorGroupAction { - public static readonly ID = 'workbench.action.newGroupBelow'; - public static readonly LABEL = nls.localize('newEditorBelow', "New Editor Group Below"); + static readonly ID = 'workbench.action.newGroupBelow'; + static readonly LABEL = nls.localize('newEditorBelow', "New Editor Group Below"); constructor( id: string, @@ -1629,4 +1637,4 @@ export class NewEditorGroupBelowAction extends BaseCreateEditorGroupAction { ) { super(id, label, GroupDirection.DOWN, editorGroupService); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index a39c9c9f3f1..3e5bdba9ba0 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -6,14 +6,14 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IListService } from 'vs/platform/list/browser/listService'; @@ -22,7 +22,8 @@ import { distinct } from 'vs/base/common/arrays'; import { IEditorGroupsService, IEditorGroup, GroupDirection, GroupLocation, GroupsOrder, preferredSideBySideGroupDirection, EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -46,6 +47,8 @@ export const SPLIT_EDITOR_RIGHT = 'workbench.action.splitEditorRight'; export const NAVIGATE_ALL_EDITORS_GROUP_PREFIX = 'edt '; export const NAVIGATE_IN_ACTIVE_GROUP_PREFIX = 'edt active '; +export const OPEN_EDITOR_AT_INDEX_COMMAND_ID = 'workbench.action.openEditorAtIndex'; + export interface ActiveEditorMoveArguments { to?: 'first' | 'last' | 'left' | 'right' | 'up' | 'down' | 'center' | 'position' | 'previous' | 'next'; by?: 'tab' | 'group'; @@ -75,7 +78,7 @@ const isActiveEditorMoveArg = function (arg: ActiveEditorMoveArguments): boolean function registerActiveEditorMoveCommand(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_ACTIVE_EDITOR_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, primary: null, handler: (accessor, args: any) => moveActiveEditor(args, accessor), @@ -138,8 +141,8 @@ function moveActiveTab(args: ActiveEditorMoveArguments, control: IEditor, access function moveActiveEditorToGroup(args: ActiveEditorMoveArguments, control: IEditor, accessor: ServicesAccessor): void { const editorGroupService = accessor.get(IEditorGroupsService); + const configurationService = accessor.get(IConfigurationService); - const groups = editorGroupService.groups; const sourceGroup = control.group; let targetGroup: IEditorGroup; @@ -179,12 +182,15 @@ function moveActiveEditorToGroup(args: ActiveEditorMoveArguments, control: IEdit break; case 'next': targetGroup = editorGroupService.findGroup({ location: GroupLocation.NEXT }, sourceGroup); + if (!targetGroup) { + targetGroup = editorGroupService.addGroup(sourceGroup, preferredSideBySideGroupDirection(configurationService)); + } break; case 'center': - targetGroup = groups[(groups.length / 2) - 1]; + targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[(editorGroupService.count / 2) - 1]; break; case 'position': - targetGroup = groups[args.value - 1]; + targetGroup = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)[args.value - 1]; break; } @@ -219,17 +225,17 @@ export function mergeAllGroups(editorGroupService: IEditorGroupsService): void { function registerDiffEditorCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.compareEditor.nextChange', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, - primary: null, + primary: KeyMod.Alt | KeyCode.F5, handler: accessor => navigateInDiffEditor(accessor, true) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.compareEditor.previousChange', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, - primary: null, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, handler: accessor => navigateInDiffEditor(accessor, false) }); @@ -244,7 +250,7 @@ function registerDiffEditorCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: TOGGLE_DIFF_INLINE_MODE, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: void 0, handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { @@ -260,9 +266,33 @@ function registerDiffEditorCommands(): void { } } }); + + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: TOGGLE_DIFF_INLINE_MODE, + title: nls.localize('toggleInlineView', "Compare: Toggle Inline View") + }, + when: ContextKeyExpr.has('textCompareEditorActive') + }); } function registerOpenEditorAtIndexCommands(): void { + const openEditorAtIndex: ICommandHandler = (accessor: ServicesAccessor, editorIndex: number): void => { + const editorService = accessor.get(IEditorService); + const activeControl = editorService.activeControl; + if (activeControl) { + const editor = activeControl.group.getEditor(editorIndex); + if (editor) { + editorService.openEditor(editor); + } + } + }; + + // This command takes in the editor index number to open as an argument + CommandsRegistry.registerCommand({ + id: OPEN_EDITOR_AT_INDEX_COMMAND_ID, + handler: openEditorAtIndex + }); // Keybindings to focus a specific index in the tab folder if tabs are enabled for (let i = 0; i < 9; i++) { @@ -270,24 +300,12 @@ function registerOpenEditorAtIndexCommands(): void { const visibleIndex = i + 1; KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.openEditorAtIndex' + visibleIndex, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + id: OPEN_EDITOR_AT_INDEX_COMMAND_ID + visibleIndex, + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyMod.Alt | toKeyCode(visibleIndex), mac: { primary: KeyMod.WinCtrl | toKeyCode(visibleIndex) }, - handler: accessor => { - const editorService = accessor.get(IEditorService); - - const activeControl = editorService.activeControl; - if (activeControl) { - const editor = activeControl.group.getEditor(editorIndex); - if (editor) { - return editorService.openEditor(editor).then(() => void 0); - } - } - - return void 0; - } + handler: accessor => openEditorAtIndex(accessor, editorIndex) }); } @@ -315,7 +333,7 @@ function registerFocusEditorGroupAtIndexCommands(): void { for (let groupIndex = 1; groupIndex < 8; groupIndex++) { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: toCommandId(groupIndex), - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyMod.CtrlCmd | toKeyCode(groupIndex), handler: accessor => { @@ -330,7 +348,7 @@ function registerFocusEditorGroupAtIndexCommands(): void { } // Group exists: just focus - const groups = editorGroupService.getGroups(GroupsOrder.CREATION_TIME); + const groups = editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); if (groups[groupIndex]) { return groups[groupIndex].focus(); } @@ -419,7 +437,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_SAVED_EDITORS_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_U), handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { @@ -439,7 +457,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_W), handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { @@ -459,7 +477,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_EDITOR_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyMod.CtrlCmd | KeyCode.KEY_W, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KEY_W] }, @@ -487,7 +505,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_EDITOR_GROUP_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext), primary: KeyMod.CtrlCmd | KeyCode.KEY_W, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KEY_W] }, @@ -508,7 +526,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: void 0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_T }, @@ -537,7 +555,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: void 0, handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { @@ -554,7 +572,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEEP_EDITOR_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter), handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { @@ -571,7 +589,7 @@ function registerCloseEditorCommands() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SHOW_EDITORS_IN_GROUP, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: void 0, handler: (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index f74ab3e5616..e962ca518aa 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -15,8 +15,7 @@ import { IPartService } from 'vs/workbench/services/part/common/partService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; -import { toWinJsPromise } from 'vs/base/common/async'; -import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; export interface IOpenEditorResult { @@ -26,17 +25,24 @@ export interface IOpenEditorResult { export class EditorControl extends Disposable { + get minimumWidth() { return this._activeControl ? this._activeControl.minimumWidth : DEFAULT_EDITOR_MIN_DIMENSIONS.width; } + get minimumHeight() { return this._activeControl ? this._activeControl.minimumHeight : DEFAULT_EDITOR_MIN_DIMENSIONS.height; } + get maximumWidth() { return this._activeControl ? this._activeControl.maximumWidth : DEFAULT_EDITOR_MAX_DIMENSIONS.width; } + get maximumHeight() { return this._activeControl ? this._activeControl.maximumHeight : DEFAULT_EDITOR_MAX_DIMENSIONS.height; } + private _onDidFocus: Emitter = this._register(new Emitter()); get onDidFocus(): Event { return this._onDidFocus.event; } - private activeControlFocusListener: IDisposable; - - private dimension: Dimension; - private editorOperation: LongRunningOperation; + private _onDidSizeConstraintsChange = this._register(new Emitter<{ width: number; height: number; }>()); + get onDidSizeConstraintsChange(): Event<{ width: number; height: number; }> { return this._onDidSizeConstraintsChange.event; } private _activeControl: BaseEditor; private controls: BaseEditor[] = []; + private activeControlDisposeables: IDisposable[] = []; + private dimension: Dimension; + private editorOperation: LongRunningOperation; + constructor( private parent: HTMLElement, private groupView: IEditorGroupView, @@ -76,16 +82,13 @@ export class EditorControl extends Disposable { // Create editor const control = this.doCreateEditorControl(descriptor); - // Remember editor as active - this._activeControl = control; + // Set editor as active + this.doSetActiveControl(control); // Show editor this.parent.appendChild(control.getContainer()); show(control.getContainer()); - // Track focus - this.activeControlFocusListener = control.onDidFocus(() => this._onDidFocus.fire()); - // Indicate to editor that it is now visible control.setVisible(true, this.groupView); @@ -129,13 +132,29 @@ export class EditorControl extends Disposable { return control; } + private doSetActiveControl(control: BaseEditor) { + this._activeControl = control; + + // Clear out previous active control listeners + this.activeControlDisposeables = dispose(this.activeControlDisposeables); + + // Listen to control changes + if (control) { + this.activeControlDisposeables.push(control.onDidSizeConstraintsChange(e => this._onDidSizeConstraintsChange.fire(e))); + this.activeControlDisposeables.push(control.onDidFocus(() => this._onDidFocus.fire())); + } + + // Indicate that size constraints could have changed due to new editor + this._onDidSizeConstraintsChange.fire(); + } + private doSetInput(control: BaseEditor, editor: EditorInput, options: EditorOptions): TPromise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same - const forceOpen = options && options.forceOpen; + const forceReload = options && options.forceReload; const inputMatches = control.input && control.input.matches(editor); - if (inputMatches && !forceOpen) { + if (inputMatches && !forceReload) { // Forward options control.setOptions(options); @@ -155,7 +174,7 @@ export class EditorControl extends Disposable { // Call into editor control const editorWillChange = !inputMatches; - return toWinJsPromise(control.setInput(editor, options, operation.token)).then(() => { + return TPromise.wrap(control.setInput(editor, options, operation.token)).then(() => { // Focus (unless prevented or another operation is running) if (operation.isCurrent()) { @@ -196,10 +215,7 @@ export class EditorControl extends Disposable { this._activeControl.setVisible(false, this.groupView); // Clear active control - this._activeControl = null; - - // Clear focus listener - this.activeControlFocusListener = dispose(this.activeControlFocusListener); + this.doSetActiveControl(null); } closeEditor(editor: EditorInput): void { @@ -223,8 +239,8 @@ export class EditorControl extends Disposable { } dispose(): void { - this.activeControlFocusListener = dispose(this.activeControlFocusListener); + this.activeControlDisposeables = dispose(this.activeControlDisposeables); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 700ed71d572..f1469891010 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -17,6 +17,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { GroupDirection, MergeGroupMode } from 'vs/workbench/services/group/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { RunOnceScheduler } from 'vs/base/common/async'; interface IDropOperation { splitDirection?: GroupDirection; @@ -25,7 +26,6 @@ interface IDropOperation { class DropOverlay extends Themable { private static OVERLAY_ID = 'monaco-workbench-editor-drop-overlay'; - private static EDGE_DISTANCE_THRESHOLD = 0.3; private container: HTMLElement; private overlay: HTMLElement; @@ -33,6 +33,8 @@ class DropOverlay extends Themable { private currentDropOperation: IDropOperation; private _disposed: boolean; + private cleanupOverlayScheduler: RunOnceScheduler; + private readonly editorTransfer = LocalSelectionTransfer.getInstance(); private readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -44,6 +46,8 @@ class DropOverlay extends Themable { ) { super(themeService); + this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); + this.create(); } @@ -58,8 +62,14 @@ class DropOverlay extends Themable { this.container = document.createElement('div'); this.container.id = DropOverlay.OVERLAY_ID; this.container.style.top = `${overlayOffsetHeight}px`; + + // Parent this.groupView.element.appendChild(this.container); - this._register(toDisposable(() => this.groupView.element.removeChild(this.container))); + addClass(this.groupView.element, 'dragged-over'); + this._register(toDisposable(() => { + this.groupView.element.removeChild(this.container); + removeClass(this.groupView.element, 'dragged-over'); + })); // Overlay this.overlay = document.createElement('div'); @@ -112,7 +122,12 @@ class DropOverlay extends Themable { } // Position overlay - this.positionOverlay(e.offsetX, e.offsetY); + this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup); + + // Make sure to stop any running cleanup scheduler to remove the overlay + if (this.cleanupOverlayScheduler.isScheduled()) { + this.cleanupOverlayScheduler.cancel(); + } }, onDragLeave: e => this.dispose(), @@ -139,9 +154,9 @@ class DropOverlay extends Themable { // To protect against this issue we always destroy the overlay as soon as we detect a // mouse event over it. The delay is used to guarantee we are not interfering with the // actual DROP event that can also trigger a mouse over event. - setTimeout(() => { - this.dispose(); - }, 300); + if (!this.cleanupOverlayScheduler.isScheduled()) { + this.cleanupOverlayScheduler.schedule(); + } })); } @@ -249,50 +264,112 @@ class DropOverlay extends Themable { return (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); } - private positionOverlay(mousePosX: number, mousePosY: number): void { - const groupViewWidth = this.groupView.element.clientWidth; - const groupViewHeight = this.groupView.element.clientHeight; + private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean): void { + const preferSplitVertically = this.accessor.partOptions.openSideBySideDirection === 'right'; - const topEdgeDistance = mousePosY; - const leftEdgeDistance = mousePosX; - const rightEdgeDistance = groupViewWidth - mousePosX; - const bottomEdgeDistance = groupViewHeight - mousePosY; + const editorControlWidth = this.groupView.element.clientWidth; + const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight(); - const edgeWidthThreshold = groupViewWidth * DropOverlay.EDGE_DISTANCE_THRESHOLD; - const edgeHeightThreshold = groupViewHeight * DropOverlay.EDGE_DISTANCE_THRESHOLD; - - // Find new split location given edge distance and thresholds - let splitDirection: GroupDirection; - switch (Math.min(topEdgeDistance, leftEdgeDistance, rightEdgeDistance, bottomEdgeDistance)) { - case topEdgeDistance: - if (topEdgeDistance < edgeHeightThreshold) { - splitDirection = GroupDirection.UP; - this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' }); - } - break; - case bottomEdgeDistance: - if (bottomEdgeDistance < edgeHeightThreshold) { - splitDirection = GroupDirection.DOWN; - this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' }); - } - break; - case leftEdgeDistance: - if (leftEdgeDistance < edgeWidthThreshold) { - splitDirection = GroupDirection.LEFT; - this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' }); - } - break; - case rightEdgeDistance: - if (rightEdgeDistance < edgeWidthThreshold) { - splitDirection = GroupDirection.RIGHT; - this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' }); - } - break; + let edgeWidthThresholdFactor: number; + if (isDraggingGroup) { + edgeWidthThresholdFactor = preferSplitVertically ? 0.3 : 0.1; // give larger threshold when dragging group depending on preferred split direction + } else { + edgeWidthThresholdFactor = 0.1; // 10% threshold to split if dragging editors } - // No split, position overlay over entire group - if (typeof splitDirection !== 'number') { - this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); + let edgeHeightThresholdFactor: number; + if (isDraggingGroup) { + edgeHeightThresholdFactor = preferSplitVertically ? 0.1 : 0.3; // give larger threshold when dragging group depending on preferred split direction + } else { + edgeHeightThresholdFactor = 0.1; // 10% threshold to split if dragging editors + } + + const edgeWidthThreshold = editorControlWidth * edgeWidthThresholdFactor; + const edgeHeightThreshold = editorControlHeight * edgeHeightThresholdFactor; + + const splitWidthThreshold = editorControlWidth / 3; // offer to split left/right at 33% + const splitHeightThreshold = editorControlHeight / 3; // offer to split up/down at 33% + + // Enable to debug the drop threshold square + // let child = this.overlay.children.item(0) as HTMLElement || this.overlay.appendChild(document.createElement('div')); + // child.style.backgroundColor = 'red'; + // child.style.position = 'absolute'; + // child.style.width = (groupViewWidth - (2 * edgeWidthThreshold)) + 'px'; + // child.style.height = (groupViewHeight - (2 * edgeHeightThreshold)) + 'px'; + // child.style.left = edgeWidthThreshold + 'px'; + // child.style.top = edgeHeightThreshold + 'px'; + + // No split if mouse is above certain threshold in the center of the view + let splitDirection: GroupDirection; + if ( + mousePosX > edgeWidthThreshold && mousePosX < editorControlWidth - edgeWidthThreshold && + mousePosY > edgeHeightThreshold && mousePosY < editorControlHeight - edgeHeightThreshold + ) { + splitDirection = void 0; + } + + // Offer to split otherwise + else { + + // User prefers to split vertically: offer a larger hitzone + // for this direction like so: + // ---------------------------------------------- + // | | SPLIT UP | | + // | SPLIT |-----------------------| SPLIT | + // | | MERGE | | + // | LEFT |-----------------------| RIGHT | + // | | SPLIT DOWN | | + // ---------------------------------------------- + if (preferSplitVertically) { + if (mousePosX < splitWidthThreshold) { + splitDirection = GroupDirection.LEFT; + } else if (mousePosX > splitWidthThreshold * 2) { + splitDirection = GroupDirection.RIGHT; + } else if (mousePosY < editorControlHeight / 2) { + splitDirection = GroupDirection.UP; + } else { + splitDirection = GroupDirection.DOWN; + } + } + + // User prefers to split horizontally: offer a larger hitzone + // for this direction like so: + // ---------------------------------------------- + // | SPLIT UP | + // |--------------------------------------------| + // | SPLIT LEFT | MERGE | SPLIT RIGHT | + // |--------------------------------------------| + // | SPLIT DOWN | + // ---------------------------------------------- + else { + if (mousePosY < splitHeightThreshold) { + splitDirection = GroupDirection.UP; + } else if (mousePosY > splitHeightThreshold * 2) { + splitDirection = GroupDirection.DOWN; + } else if (mousePosX < editorControlWidth / 2) { + splitDirection = GroupDirection.LEFT; + } else { + splitDirection = GroupDirection.RIGHT; + } + } + } + + // Draw overlay based on split direction + switch (splitDirection) { + case GroupDirection.UP: + this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' }); + break; + case GroupDirection.DOWN: + this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' }); + break; + case GroupDirection.LEFT: + this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' }); + break; + case GroupDirection.RIGHT: + this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' }); + break; + default: + this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); } // Make sure the overlay is visible now @@ -306,6 +383,16 @@ class DropOverlay extends Themable { } private doPositionOverlay(options: { top: string, left: string, width: string, height: string }): void { + + // Container + const offsetHeight = this.getOverlayOffsetHeight(); + if (offsetHeight) { + this.container.style.height = `calc(100% - ${offsetHeight}px)`; + } else { + this.container.style.height = '100%'; + } + + // Overlay this.overlay.style.top = options.top; this.overlay.style.left = options.left; this.overlay.style.width = options.width; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 4823e1f0839..3d7d84cc022 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -9,7 +9,7 @@ import 'vs/css!./media/editorgroupview'; import { TPromise } from 'vs/base/common/winjs.base'; import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { EditorInput, EditorOptions, GroupIdentifier, ConfirmResult, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext } from 'vs/workbench/common/editor'; -import { Event, Emitter, once } from 'vs/base/common/event'; +import { Event, Emitter, once, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -34,7 +34,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, EDITOR_TITLE_HEIGHT, EDITOR_MIN_DIMENSIONS, EDITOR_MAX_DIMENSIONS, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { join } from 'vs/base/common/paths'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -197,6 +197,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Editor control this.editorControl = this._register(this.scopedInstantiationService.createInstance(EditorControl, this.editorContainer, this)); + this._onDidChange.input = this.editorControl.onDidSizeConstraintsChange; // Track Focus this.doTrackFocus(); @@ -336,8 +337,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { target = (e as GestureEvent).initialTarget as HTMLElement; } - if (findParentWithClass(target, 'monaco-action-bar', this.titleContainer)) { - return; // not when clicking on actions + if (findParentWithClass(target, 'monaco-action-bar', this.titleContainer) || + findParentWithClass(target, 'monaco-breadcrumb-item', this.titleContainer) + ) { + return; // not when clicking on actions or breadcrumbs } // timeout to keep focus in editor after mouse up @@ -720,6 +723,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { openEditor(editor: EditorInput, options?: EditorOptions): TPromise { + // Guard against invalid inputs + if (!editor) { + return TPromise.as(void 0); + } + // Editor opening event allows for prevention const event = new EditorOpeningEvent(this._group.id, editor, options); this._onWillOpenEditor.fire(event); @@ -748,17 +756,20 @@ export class EditorGroupView extends Themable implements IEditorGroupView { openEditorOptions.active = true; } + // Set group active unless we open inactive or preserve focus + // Do this before we open the editor in the group to prevent a false + // active editor change event before the editor is loaded + // (see https://github.com/Microsoft/vscode/issues/51679) + if (openEditorOptions.active && (!options || !options.preserveFocus)) { + this.accessor.activateGroup(this); + } + // Update model this._group.openEditor(editor, openEditorOptions); // Show editor const showEditorResult = this.doShowEditor(editor, openEditorOptions.active, options); - // Set group active unless we open inactive or preserve focus - if (openEditorOptions.active && (!options || !options.preserveFocus)) { - this.accessor.activateGroup(this); - } - return showEditorResult; } @@ -1274,14 +1285,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (activeReplacement) { // Open replacement as active editor - return this.doOpenEditor(activeReplacement.replacement, activeReplacement.options).then(() => { + const openEditorResult = this.doOpenEditor(activeReplacement.replacement, activeReplacement.options); - // Close previous active editor - this.doCloseInactiveEditor(activeReplacement.editor); + // Close previous active editor + this.doCloseInactiveEditor(activeReplacement.editor); - // Forward to title control - this.titleAreaControl.closeEditor(activeReplacement.editor); - }); + // Forward to title control + this.titleAreaControl.closeEditor(activeReplacement.editor); + + return openEditorResult; } return TPromise.as(void 0); @@ -1294,9 +1306,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region Themable protected updateStyles(): void { + const isEmpty = this.isEmpty(); // Container - if (this.isEmpty()) { + if (isEmpty) { this.element.style.backgroundColor = this.getColor(EDITOR_GROUP_EMPTY_BACKGROUND); } else { this.element.style.backgroundColor = null; @@ -1305,10 +1318,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control const { showTabs } = this.accessor.partOptions; const borderColor = this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER) || this.getColor(contrastBorder); + + if (!isEmpty && showTabs && borderColor) { + addClass(this.titleContainer, 'title-border-bottom'); + this.titleContainer.style.setProperty('--title-border-bottom-color', borderColor.toString()); + } else { + removeClass(this.titleContainer, 'title-border-bottom'); + this.titleContainer.style.removeProperty('--title-border-bottom-color'); + } + this.titleContainer.style.backgroundColor = this.getColor(showTabs ? EDITOR_GROUP_HEADER_TABS_BACKGROUND : EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND); - this.titleContainer.style.borderBottomWidth = (borderColor && showTabs) ? '1px' : null; - this.titleContainer.style.borderBottomStyle = (borderColor && showTabs) ? 'solid' : null; - this.titleContainer.style.borderBottomColor = showTabs ? borderColor : null; // Editor container this.editorContainer.style.backgroundColor = this.getColor(editorBackground); @@ -1320,19 +1339,27 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly element: HTMLElement = document.createElement('div'); - readonly minimumWidth = EDITOR_MIN_DIMENSIONS.width; - readonly minimumHeight = EDITOR_MIN_DIMENSIONS.height; - readonly maximumWidth = EDITOR_MAX_DIMENSIONS.width; - readonly maximumHeight = EDITOR_MAX_DIMENSIONS.height; + get minimumWidth(): number { return this.editorControl.minimumWidth; } + get minimumHeight(): number { return this.editorControl.minimumHeight; } + get maximumWidth(): number { return this.editorControl.maximumWidth; } + get maximumHeight(): number { return this.editorControl.maximumHeight; } - get onDidChange() { return Event.None; } // only needed if minimum sizes ever change + private _onDidChange = this._register(new Relay<{ width: number; height: number; }>()); + readonly onDidChange: Event<{ width: number; height: number; }> = this._onDidChange.event; layout(width: number, height: number): void { this.dimension = new Dimension(width, height); // Forward to controls - this.titleAreaControl.layout(new Dimension(this.dimension.width, EDITOR_TITLE_HEIGHT)); - this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - EDITOR_TITLE_HEIGHT)); + this.titleAreaControl.layout(new Dimension(this.dimension.width, this.titleAreaControl.getPreferredHeight())); + this.editorControl.layout(new Dimension(this.dimension.width, this.dimension.height - this.titleAreaControl.getPreferredHeight())); + } + + relayout(): void { + if (this.dimension) { + const { width, height } = this.dimension; + this.layout(width, height); + } } toJSON(): ISerializedEditorGroup { @@ -1351,6 +1378,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onWillDispose.fire(); this.titleAreaControl.dispose(); + // this.editorControl = null; super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 339dcc7d78d..eac59365565 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -8,32 +8,31 @@ import 'vs/workbench/browser/parts/editor/editor.contribution'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; -import { Dimension, isAncestor, toggleClass, addClass, clearNode } from 'vs/base/browser/dom'; -import { Event, Emitter, once } from 'vs/base/common/event'; +import { Dimension, isAncestor, toggleClass, addClass, $ } from 'vs/base/browser/dom'; +import { Event, Emitter, once, Relay, anyEvent } 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, ISerializedNode, GridBranchNode, isGridBranchNode, GridNode } from 'vs/base/browser/ui/grid/grid'; +import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } 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 } from 'vs/workbench/common/theme'; +import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct } from 'vs/base/common/arrays'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, EDITOR_MAX_DIMENSIONS, EDITOR_MIN_DIMENSIONS, EditorGroupsServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, EditorGroupsServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; 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 { IStorageService } from 'vs/platform/storage/common/storage'; import { Scope } from 'vs/workbench/common/memento'; import { ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { TValueCallback, TPromise } from 'vs/base/common/winjs.base'; import { always } from 'vs/base/common/async'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IWindowService } from 'vs/platform/windows/common/windows'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; 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'; interface IEditorPartUIState { serializedGrid: ISerializedGrid; @@ -41,11 +40,54 @@ interface IEditorPartUIState { mostRecentActiveGroups: GroupIdentifier[]; } +class GridWidgetView implements IView { + + readonly element: HTMLElement = $('.grid-view-container'); + + get minimumWidth(): number { return this.gridWidget ? this.gridWidget.minimumWidth : 0; } + get maximumWidth(): number { return this.gridWidget ? this.gridWidget.maximumWidth : Number.POSITIVE_INFINITY; } + get minimumHeight(): number { return this.gridWidget ? this.gridWidget.minimumHeight : 0; } + get maximumHeight(): number { return this.gridWidget ? this.gridWidget.maximumHeight : Number.POSITIVE_INFINITY; } + + private _onDidChange = new Relay<{ width: number; height: number; }>(); + readonly onDidChange: Event<{ width: number; height: number; }> = this._onDidChange.event; + + private _gridWidget: Grid; + + get gridWidget(): Grid { + return this._gridWidget; + } + + set gridWidget(grid: Grid) { + this.element.innerHTML = ''; + + if (grid) { + this.element.appendChild(grid.element); + this._onDidChange.input = grid.onDidChange; + } else { + this._onDidChange.input = Event.None; + } + + this._gridWidget = grid; + } + + layout(width: number, height: number): void { + if (this.gridWidget) { + this.gridWidget.layout(width, height); + } + } + + dispose(): void { + this._onDidChange.dispose(); + } +} + export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor { _serviceBrand: any; private static readonly EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.state'; + private static readonly EDITOR_PART_CENTERED_VIEW_STORAGE_KEY = 'editorpart.centeredview'; //#region Events @@ -64,6 +106,10 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor private _onDidMoveGroup: Emitter = this._register(new Emitter()); get onDidMoveGroup(): Event { return this._onDidMoveGroup.event; } + private onDidSetGridWidget = this._register(new Emitter<{ width: number; height: number; }>()); + private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; }>()); + get onDidSizeConstraintsChange(): Event<{ width: number; height: number; }> { return anyEvent(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); } + private _onDidPreferredSizeChange: Emitter = this._register(new Emitter()); get onDidPreferredSizeChange(): Event { return this._onDidPreferredSizeChange.event; } @@ -73,6 +119,8 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor private _preferredSize: Dimension; private memento: object; + private globalMemento: object; + private _partOptions: IEditorPartOptions; private _activeGroup: IEditorGroupView; @@ -80,28 +128,28 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor private mostRecentActiveGroups: GroupIdentifier[] = []; private container: HTMLElement; + private centeredLayoutWidget: CenteredViewLayout; private gridWidget: SerializableGrid; + private gridWidgetView: GridWidgetView; private _whenRestored: TPromise; private whenRestoredComplete: TValueCallback; - private previousUIState: IEditorPartUIState; - constructor( id: string, private restorePreviousState: boolean, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService private configurationService: IConfigurationService, - @IStorageService private storageService: IStorageService, - @INotificationService private notificationService: INotificationService, - @IWindowService private windowService: IWindowService, - @ILifecycleService private lifecycleService: ILifecycleService + @IStorageService private storageService: IStorageService ) { super(id, { hasTitle: false }, themeService); + this.gridWidgetView = new GridWidgetView(); + this._partOptions = getEditorPartOptions(this.configurationService.getValue()); this.memento = this.getMemento(this.storageService, Scope.WORKSPACE); + this.globalMemento = this.getMemento(this.storageService, Scope.GLOBAL); this._whenRestored = new TPromise(resolve => { this.whenRestoredComplete = resolve; @@ -239,7 +287,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor private doFindGroupByLocation(location: GroupLocation, source: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView { const sourceGroupView = this.assertGroupView(source); - const groups = this.getGroups(GroupsOrder.CREATION_TIME); + const groups = this.getGroups(GroupsOrder.GRID_APPEARANCE); const index = groups.indexOf(sourceGroupView); switch (location) { @@ -294,25 +342,12 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Even all group sizes if (arrangement === GroupsArrangement.EVEN) { - this.groups.forEach(group => { - this.gridWidget.resetViewSize(group); - }); + this.gridWidget.distributeViewSizes(); } // Maximize the current active group else { - this.groups.forEach(group => { - const orientation = this.gridWidget.getOrientation(group); - - let newSize: number; - if (this.activeGroup === group) { - newSize = orientation === Orientation.HORIZONTAL ? EDITOR_MAX_DIMENSIONS.width : EDITOR_MAX_DIMENSIONS.height; - } else { - newSize = orientation === Orientation.HORIZONTAL ? EDITOR_MIN_DIMENSIONS.width : EDITOR_MIN_DIMENSIONS.height; - } - - this.gridWidget.resizeView(group, newSize); - }); + this.gridWidget.maximizeViewSize(this.activeGroup); } } @@ -332,85 +367,85 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor applyLayout(layout: EditorGroupLayout): void { const gridHasFocus = isAncestor(document.activeElement, this.container); - const groupsInGridOrder = this.getGroups(GroupsOrder.GRID_APPEARANCE); // Determine how many groups we need overall - let groupsInLayout = 0; + let layoutGroupsCount = 0; function countGroups(groups: GroupLayoutArgument[]): void { groups.forEach(group => { if (Array.isArray(group.groups)) { countGroups(group.groups); } else { - groupsInLayout++; + layoutGroupsCount++; } }); } countGroups(layout.groups); // If we currently have too many groups, merge them into the last one - if (groupsInLayout < groupsInGridOrder.length) { - const lastGroupInLayout = groupsInGridOrder[groupsInLayout - 1]; - groupsInGridOrder.forEach((group, index) => { - if (index >= groupsInLayout) { + let currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE); + if (layoutGroupsCount < currentGroupViews.length) { + const lastGroupInLayout = currentGroupViews[layoutGroupsCount - 1]; + currentGroupViews.forEach((group, index) => { + if (index >= layoutGroupsCount) { this.mergeGroup(group, lastGroupInLayout); } }); + + currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE); } - // Apply orientation - if (typeof layout.orientation === 'number') { - this.setGroupOrientation(layout.orientation); - } + const activeGroup = this.activeGroup; - // Build layout - let currentGroupIndex = 0; - const buildLayout = (groups: IEditorGroupView[], descriptions: GroupLayoutArgument[], direction: GroupDirection) => { - if (descriptions.length === 0) { - return; // we need at least one group to layout + // Prepare grid descriptor to create new grid from + const gridDescriptor = createSerializedGrid({ + orientation: this.toGridViewOrientation( + layout.orientation, + this.isTwoDimensionalGrid() ? + this.gridWidget.orientation : // preserve original orientation for 2-dimensional grids + orthogonal(this.gridWidget.orientation) // otherwise flip (fix https://github.com/Microsoft/vscode/issues/52975) + ), + groups: layout.groups + }); + + // Recreate gridwidget with descriptor + this.doCreateGridControlWithState(gridDescriptor, activeGroup.id, currentGroupViews); + + // Layout + this.doLayout(this.dimension); + + // Update container + this.updateContainer(); + + // Mark preferred size as changed + this.resetPreferredSize(); + + // Events for groups that got added + this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => { + if (currentGroupViews.indexOf(groupView) === -1) { + this._onDidAddGroup.fire(groupView); } + }); - // Either move existing or add a new group for each item in the description - let totalProportions = 0; - descriptions.forEach((description, index) => { - if (index > 0) { - currentGroupIndex++; - const existingGroup = groupsInGridOrder[currentGroupIndex]; - if (existingGroup) { - groups.push(this.moveGroup(existingGroup, groups[index - 1], direction)); - } else { - groups.push(this.addGroup(groups[index - 1], direction)); - } - } + // Update labels + this.updateGroupLabels(); - if (typeof description.size === 'number') { - totalProportions += description.size; - } - }); - - // Apply proportions if they are valid (sum() === 1) - if (totalProportions === 1) { - const totalSize = groups.map(group => this.getSize(group)).reduce(((prev, cur) => prev + cur)); - descriptions.forEach((description, index) => { - this.setSize(groups[index], totalSize * description.size); - }); - } - - // Continue building layout if description.groups is array-type - descriptions.forEach((description, index) => { - if (Array.isArray(description.groups)) { - buildLayout([groups[index]], description.groups, direction === GroupDirection.RIGHT ? GroupDirection.DOWN : GroupDirection.RIGHT); - } - }); - }; - - buildLayout([groupsInGridOrder[0]], layout.groups, this.orientation === GroupOrientation.HORIZONTAL ? GroupDirection.RIGHT : GroupDirection.DOWN); - - // Restore Focus + // Restore focus as needed if (gridHasFocus) { this._activeGroup.focus(); } } + private isTwoDimensionalGrid(): boolean { + const views = this.gridWidget.getViews(); + if (isGridBranchNode(views)) { + // the grid is 2-dimensional if any children + // of the grid is a branch node + return views.children.some(child => isGridBranchNode(child)); + } + + return false; + } + addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroupView { const locationView = this.assertGroupView(location); @@ -443,6 +478,9 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Event this._onDidAddGroup.fire(newGroupView); + // Update labels + this.updateGroupLabels(); + return newGroupView; } @@ -506,6 +544,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Mark group as new active group.setActive(true); + // Maximize the group if it is currently minimized + if (this.gridWidget) { + const viewSize = this.gridWidget.getViewSize2(group); + if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) { + this.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); + } + } + // Event this._onDidActiveGroupChange.fire(group); } @@ -533,6 +579,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } } + private toGridViewOrientation(orientation: GroupOrientation, fallback?: Orientation): Orientation { + if (typeof orientation === 'number') { + return orientation === GroupOrientation.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } + + return fallback; + } + removeGroup(group: IEditorGroupView | GroupIdentifier): void { const groupView = this.assertGroupView(group); if (this.groupViews.size === 1) { @@ -583,12 +637,8 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this._activeGroup.focus(); } - // Update labels: since our labels are created using the index of the - // group, removing a group might produce gaps. So we iterate over all - // groups and reassign the label based on the index. - this.getGroups(GroupsOrder.CREATION_TIME).forEach((group, index) => { - group.setLabel(this.getGroupLabel(index + 1)); - }); + // Update labels + this.updateGroupLabels(); // Update container this.updateContainer(); @@ -600,10 +650,6 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this._onDidRemoveGroup.fire(groupView); } - private getGroupLabel(index: number): string { - return localize('groupLabel', "Group {0}", index); - } - moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView { const sourceView = this.assertGroupView(group); const targetView = this.assertGroupView(location); @@ -654,7 +700,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Move/Copy editors over into target let index = (options && typeof options.index === 'number') ? options.index : targetView.count; sourceView.editors.forEach(editor => { - const inactive = !sourceView.isActive(editor); + const inactive = !sourceView.isActive(editor) || this._activeGroup !== sourceView; const copyOptions: ICopyEditorOptions = { index, inactive, preserveFocus: inactive }; if (options && options.mode === MergeGroupMode.COPY_EDITORS) { @@ -690,6 +736,11 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor //#region Part + get minimumWidth(): number { return this.centeredLayoutWidget.minimumWidth; } + get maximumWidth(): number { return this.centeredLayoutWidget.maximumWidth; } + get minimumHeight(): number { return this.centeredLayoutWidget.minimumHeight; } + get maximumHeight(): number { return this.centeredLayoutWidget.maximumHeight; } + get preferredSize(): Dimension { if (!this._preferredSize) { this._preferredSize = new Dimension(this.gridWidget.minimumWidth, this.gridWidget.minimumHeight); @@ -707,14 +758,16 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this._onDidPreferredSizeChange.fire(); } - private getGridSeparatorBorder(): Color { + private get gridSeparatorBorder(): Color { return this.theme.getColor(EDITOR_GROUP_BORDER) || this.theme.getColor(contrastBorder) || Color.transparent; } protected updateStyles(): void { this.container.style.backgroundColor = this.getColor(editorBackground); - this.gridWidget.style({ separatorBorder: this.getGridSeparatorBorder() }); + const separatorBorderStyle = { separatorBorder: this.gridSeparatorBorder, background: this.theme.getColor(EDITOR_PANE_BACKGROUND) || Color.transparent }; + this.gridWidget.style(separatorBorderStyle); + this.centeredLayoutWidget.styles(separatorBorderStyle); } createContentArea(parent: HTMLElement): HTMLElement { @@ -724,8 +777,10 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor addClass(this.container, 'content'); parent.appendChild(this.container); - // Grid control - this.doCreateGridControl(this.container); + // Grid control with center layout + this.doCreateGridControl(); + + this.centeredLayoutWidget = this._register(new CenteredViewLayout(this.container, this.gridWidgetView, this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY])); // Drop support this._register(this.instantiationService.createInstance(EditorDropTarget, this, this.container)); @@ -733,17 +788,25 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor return this.container; } - private doCreateGridControl(container: HTMLElement): void { + centerLayout(active: boolean): void { + this.centeredLayoutWidget.activate(active); + } + + isLayoutCentered(): boolean { + return this.centeredLayoutWidget.isActive(); + } + + private doCreateGridControl(): void { // Grid Widget (with previous UI state) if (this.restorePreviousState) { - this.doCreateGridControlWithPreviousState(container); + this.doCreateGridControlWithPreviousState(); } // Grid Widget (no previous UI state or failed to restore) if (!this.gridWidget) { const initialGroup = this.doCreateGroupView(); - this.gridWidget = this._register(new SerializableGrid(container, initialGroup)); + this.doSetGridWidget(new SerializableGrid(initialGroup)); // Ensure a group is active this.doSetGroupActive(initialGroup); @@ -756,200 +819,107 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this.updateContainer(); } - private doCreateGridControlWithPreviousState(container: HTMLElement): void { - const uiState = this.doGetPreviousState(); + private doCreateGridControlWithPreviousState(): void { + const uiState = this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState; if (uiState && uiState.serializedGrid) { - try { - this.previousUIState = uiState; - // MRU - this.mostRecentActiveGroups = uiState.mostRecentActiveGroups; + // MRU + this.mostRecentActiveGroups = uiState.mostRecentActiveGroups; - // Grid Widget - this.gridWidget = this._register(SerializableGrid.deserialize(container, uiState.serializedGrid, { - fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => { - const groupView = this.doCreateGroupView(serializedEditorGroup); - if (groupView.id === uiState.activeGroup) { - this.doSetGroupActive(groupView); - } + // Grid Widget + this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup); - return groupView; - } - }, { styles: { separatorBorder: this.getGridSeparatorBorder() } })); - - // Ensure last active group has focus - this._activeGroup.focus(); - } catch (error) { - if (this.gridWidget) { - this.gridWidget.dispose(); - this.gridWidget = void 0; - } - - clearNode(container); - this.groupViews.forEach(group => group.dispose()); - this.groupViews.clear(); - this._activeGroup = void 0; - this.mostRecentActiveGroups = []; - - this.gridError(error); // TODO@ben remove this safe guard once the grid is stable - } + // Ensure last active group has focus + this._activeGroup.focus(); } } - private doGetPreviousState(): IEditorPartUIState { - const legacyState = this.doGetPreviousLegacyState(); - if (legacyState) { - return legacyState; // TODO@ben remove after a while + private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + + // Determine group views to reuse if any + let reuseGroupViews: IEditorGroupView[]; + if (editorGroupViewsToReuse) { + reuseGroupViews = editorGroupViewsToReuse.slice(0); // do not modify original array + } else { + reuseGroupViews = []; } - return this.memento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState; - } - - private doGetPreviousLegacyState(): IEditorPartUIState { - const LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.uiState'; - const LEGACY_STACKS_MODEL_STORAGE_KEY = 'editorStacks.model'; - - interface ILegacyEditorPartUIState { - ratio: number[]; - groupOrientation: 'vertical' | 'horizontal'; - } - - interface ISerializedLegacyEditorStacksModel { - groups: ISerializedEditorGroup[]; - active: number; - } - - let legacyUIState: ISerializedLegacyEditorStacksModel; - const legacyUIStateRaw = this.storageService.get(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE); - if (legacyUIStateRaw) { - try { - legacyUIState = JSON.parse(legacyUIStateRaw); - } catch (error) { /* ignore */ } - } - - if (legacyUIState) { - this.storageService.remove(LEGACY_STACKS_MODEL_STORAGE_KEY, StorageScope.WORKSPACE); - } - - const legacyPartState = this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY] as ILegacyEditorPartUIState; - if (legacyPartState) { - delete this.memento[LEGACY_EDITOR_PART_UI_STATE_STORAGE_KEY]; - } - - if (legacyUIState && Array.isArray(legacyUIState.groups) && legacyUIState.groups.length > 0) { - const splitHorizontally = legacyPartState && legacyPartState.groupOrientation === 'horizontal'; - - const legacyState: IEditorPartUIState = Object.create(null); - - const positionOneGroup = legacyUIState.groups[0]; - const positionTwoGroup = legacyUIState.groups[1]; - const positionThreeGroup = legacyUIState.groups[2]; - - legacyState.activeGroup = legacyUIState.active; - legacyState.mostRecentActiveGroups = [legacyUIState.active]; - - if (positionTwoGroup || positionThreeGroup) { - if (!positionThreeGroup) { - legacyState.mostRecentActiveGroups.push(legacyState.activeGroup === 0 ? 1 : 0); + // Create new + const gridWidget = SerializableGrid.deserialize(serializedGrid, { + fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => { + let groupView: IEditorGroupView; + if (reuseGroupViews.length > 0) { + groupView = reuseGroupViews.shift(); } else { - if (legacyState.activeGroup === 0) { - legacyState.mostRecentActiveGroups.push(1, 2); - } else if (legacyState.activeGroup === 1) { - legacyState.mostRecentActiveGroups.push(0, 2); - } else { - legacyState.mostRecentActiveGroups.push(0, 1); - } - } - } - - const toNode = function (group: ISerializedEditorGroup, size: number): ISerializedNode { - return { - data: group, - size, - type: 'leaf' - }; - }; - - const baseSize = 1200; // just some number because layout() was not called yet, but we only need the proportions - - // No split editor - if (!positionTwoGroup) { - legacyState.serializedGrid = { - width: baseSize, - height: baseSize, - orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL, - root: toNode(positionOneGroup, baseSize) - }; - } - - // Split editor (2 or 3 columns) - else { - const children: ISerializedNode[] = []; - - const size = positionThreeGroup ? baseSize / 3 : baseSize / 2; - - children.push(toNode(positionOneGroup, size)); - children.push(toNode(positionTwoGroup, size)); - - if (positionThreeGroup) { - children.push(toNode(positionThreeGroup, size)); + groupView = this.doCreateGroupView(serializedEditorGroup); } - legacyState.serializedGrid = { - width: baseSize, - height: baseSize, - orientation: splitHorizontally ? Orientation.VERTICAL : Orientation.HORIZONTAL, - root: { - data: children, - size: baseSize, - type: 'branch' - } - }; - } + if (groupView.id === activeGroupId) { + this.doSetGroupActive(groupView); + } - return legacyState; + return groupView; + } + }, { styles: { separatorBorder: this.gridSeparatorBorder } }); + + // Set it + this.doSetGridWidget(gridWidget); + } + + private doSetGridWidget(gridWidget?: SerializableGrid): void { + if (this.gridWidget) { + this.gridWidget.dispose(); } - return void 0; + this.gridWidget = gridWidget; + this.gridWidgetView.gridWidget = gridWidget; + + if (gridWidget) { + this._onDidSizeConstraintsChange.input = gridWidget.onDidChange; + } + + this.onDidSetGridWidget.fire(); } private updateContainer(): void { toggleClass(this.container, 'empty', this.isEmpty()); } - private isEmpty(): boolean { - return this.groupViews.size === 1 && this._activeGroup.isEmpty(); + private updateGroupLabels(): void { + + // Since our labels are created using the index of the + // group, adding/removing a group might produce gaps. + // So we iterate over all groups and reassign the label + // based on the index. + this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach((group, index) => { + group.setLabel(this.getGroupLabel(index + 1)); + }); } - // TODO@ben this should be removed once the gridwidget is stable - private gridError(error: Error): void { - console.error(error); + private getGroupLabel(index: number): string { + return localize('groupLabel', "Group {0}", index); + } - if (this.previousUIState) { - console.error('Serialized Grid State: ', this.previousUIState); - } - - this.lifecycleService.when(LifecyclePhase.Running).then(() => { - this.notificationService.prompt(Severity.Error, `Grid Issue: ${error}. Please report this error stack with reproducible steps.`, [{ label: 'Open DevTools', run: () => this.windowService.openDevTools() }]); - }); + private isEmpty(): boolean { + return this.groupViews.size === 1 && this._activeGroup.isEmpty(); } layout(dimension: Dimension): Dimension[] { const sizes = super.layout(dimension); - this.dimension = sizes[1]; + this.doLayout(sizes[1]); + + return sizes; + } + + private doLayout(dimension: Dimension): void { + this.dimension = dimension; // Layout Grid - try { - this.gridWidget.layout(this.dimension.width, this.dimension.height); - } catch (error) { - this.gridError(error); - } + this.centeredLayoutWidget.layout(this.dimension.width, this.dimension.height); // Event this._onDidLayout.fire(dimension); - - return sizes; } shutdown(): void { @@ -969,6 +939,9 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } } + // Persist centered view state + this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = this.centeredLayoutWidget.state; + // Forward to all groups this.groupViews.forEach(group => group.shutdown()); @@ -981,8 +954,13 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this.groupViews.forEach(group => group.dispose()); this.groupViews.clear(); + // Grid widget + if (this.gridWidget) { + this.gridWidget.dispose(); + } + super.dispose(); } //#endregion -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index a1b7a48374d..a5fe420b742 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/editorpicker'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IAutoFocus, Mode, IEntryRunContext, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup, QuickOpenItemAccessor } from 'vs/base/parts/quickopen/browser/quickOpenModel'; @@ -20,6 +20,7 @@ import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupsOrder } from 'v import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorInput, toResource } from 'vs/workbench/common/editor'; import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class EditorPickerEntry extends QuickOpenEntryGroup { @@ -32,38 +33,38 @@ export class EditorPickerEntry extends QuickOpenEntryGroup { super(); } - public getLabelOptions(): IIconLabelValueOptions { + getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.getResource()), italic: !this._group.isPinned(this.editor) }; } - public getLabel(): string { + getLabel(): string { return this.editor.getName(); } - public getIcon(): string { + getIcon(): string { return this.editor.isDirty() ? 'dirty' : ''; } - public get group(): IEditorGroup { + get group(): IEditorGroup { return this._group; } - public getResource(): URI { + getResource(): URI { return toResource(this.editor, { supportSideBySide: true }); } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, editor group picker", this.getLabel()); } - public getDescription(): string { + getDescription(): string { return this.editor.getDescription(); } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { return this.runOpen(context); } @@ -91,7 +92,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { this.scorerCache = Object.create(null); } - public getResults(searchValue: string): TPromise { + getResults(searchValue: string, token: CancellationToken): TPromise { const editorEntries = this.getEditorEntries(); if (!editorEntries.length) { return TPromise.as(null); @@ -117,7 +118,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { // Sorting if (query.value) { - const groups = this.editorGroupService.getGroups(GroupsOrder.CREATION_TIME); + const groups = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); entries.sort((e1, e2) => { if (e1.group !== e2.group) { return groups.indexOf(e1.group) - groups.indexOf(e2.group); // older groups first @@ -142,7 +143,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { return TPromise.as(new QuickOpenModel(entries)); } - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { this.scorerCache = Object.create(null); } @@ -151,7 +152,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { export class ActiveEditorGroupPicker extends BaseEditorPicker { - public static readonly ID = 'workbench.picker.activeEditors'; + static readonly ID = 'workbench.picker.activeEditors'; protected getEditorEntries(): EditorPickerEntry[] { return this.group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map((editor, index) => this.instantiationService.createInstance(EditorPickerEntry, editor, this.group)); @@ -161,7 +162,7 @@ export class ActiveEditorGroupPicker extends BaseEditorPicker { return this.editorGroupService.activeGroup; } - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { if (searchString) { return nls.localize('noResultsFoundInGroup', "No matching opened editor found in group"); } @@ -169,7 +170,7 @@ export class ActiveEditorGroupPicker extends BaseEditorPicker { return nls.localize('noOpenedEditors', "List of opened editors is currently empty in group"); } - public getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { + getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { if (searchValue || !context.quickNavigateConfiguration) { return { autoFocusFirstEntry: true @@ -201,12 +202,12 @@ export class ActiveEditorGroupPicker extends BaseEditorPicker { export class AllEditorsPicker extends BaseEditorPicker { - public static readonly ID = 'workbench.picker.editors'; + static readonly ID = 'workbench.picker.editors'; protected getEditorEntries(): EditorPickerEntry[] { const entries: EditorPickerEntry[] = []; - this.editorGroupService.getGroups(GroupsOrder.CREATION_TIME).forEach(group => { + this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => { group.editors.forEach(editor => { entries.push(this.instantiationService.createInstance(EditorPickerEntry, editor, group)); }); @@ -215,7 +216,7 @@ export class AllEditorsPicker extends BaseEditorPicker { return entries; } - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { if (searchString) { return nls.localize('noResultsFound', "No matching opened editor found"); } @@ -223,7 +224,7 @@ export class AllEditorsPicker extends BaseEditorPicker { return nls.localize('noOpenedEditorsAllGroups', "List of opened editors is currently empty"); } - public getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { + getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { if (searchValue) { return { autoFocusFirstEntry: true diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 525764c38fa..63896f3611d 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -12,8 +12,7 @@ import { $, append, runAtThisOrScheduleAtNextAnimationFrame, addDisposableListen import * as strings from 'vs/base/common/strings'; import * as paths from 'vs/base/common/paths'; import * as types from 'vs/base/common/types'; -import uri from 'vs/base/common/uri'; -import * as errors from 'vs/base/common/errors'; +import { URI as uri } from 'vs/base/common/uri'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { Action } from 'vs/base/common/actions'; import { language, LANGUAGE_DEFAULT, AccessibilitySupport } from 'vs/base/common/platform'; @@ -31,7 +30,7 @@ import { IndentUsingSpaces, IndentUsingTabs, DetectIndentation, IndentationToSpa import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { SUPPORTED_ENCODINGS, IFileService, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -58,15 +57,18 @@ import { Schemas } from 'vs/base/common/network'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Themable } from 'vs/workbench/common/theme'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { getIconClasses } from 'vs/workbench/browser/labels'; +import { timeout } from 'vs/base/common/async'; class SideBySideEditorEncodingSupport implements IEncodingSupport { constructor(private master: IEncodingSupport, private details: IEncodingSupport) { } - public getEncoding(): string { + getEncoding(): string { return this.master.getEncoding(); // always report from modified (right hand) side } - public setEncoding(encoding: string, mode: EncodingMode): void { + setEncoding(encoding: string, mode: EncodingMode): void { [this.master, this.details].forEach(s => s.setEncoding(encoding, mode)); } } @@ -128,7 +130,7 @@ class StateChange { this.metadata = false; } - public combine(other: StateChange) { + combine(other: StateChange) { this.indentation = this.indentation || other.indentation; this.selectionStatus = this.selectionStatus || other.selectionStatus; this.mode = this.mode || other.mode; @@ -153,28 +155,28 @@ interface StateDelta { class State { private _selectionStatus: string; - public get selectionStatus(): string { return this._selectionStatus; } + get selectionStatus(): string { return this._selectionStatus; } private _mode: string; - public get mode(): string { return this._mode; } + get mode(): string { return this._mode; } private _encoding: string; - public get encoding(): string { return this._encoding; } + get encoding(): string { return this._encoding; } private _EOL: string; - public get EOL(): string { return this._EOL; } + get EOL(): string { return this._EOL; } private _indentation: string; - public get indentation(): string { return this._indentation; } + get indentation(): string { return this._indentation; } private _tabFocusMode: boolean; - public get tabFocusMode(): boolean { return this._tabFocusMode; } + get tabFocusMode(): boolean { return this._tabFocusMode; } private _screenReaderMode: boolean; - public get screenReaderMode(): boolean { return this._screenReaderMode; } + get screenReaderMode(): boolean { return this._screenReaderMode; } private _metadata: string; - public get metadata(): string { return this._metadata; } + get metadata(): string { return this._metadata; } constructor() { this._selectionStatus = null; @@ -186,7 +188,7 @@ class State { this._metadata = null; } - public update(update: StateDelta): StateChange { + update(update: StateDelta): StateChange { const e = new StateChange(); let somethingChanged = false; @@ -277,7 +279,6 @@ function hide(el: HTMLElement): void { } export class EditorStatus implements IStatusbarItem { - private state: State; private element: HTMLElement; private tabFocusModeElement: HTMLElement; @@ -308,7 +309,7 @@ export class EditorStatus implements IStatusbarItem { this.state = new State(); } - public render(container: HTMLElement): IDisposable { + render(container: HTMLElement): IDisposable { this.element = append(container, $('.editor-statusbar-item')); this.tabFocusModeElement = append(this.element, $('a.editor-status-tabfocusmode.status-bar-info')); @@ -492,13 +493,13 @@ export class EditorStatus implements IStatusbarItem { private onModeClick(): void { const action = this.instantiationService.createInstance(ChangeModeAction, ChangeModeAction.ID, ChangeModeAction.LABEL); - action.run().done(null, errors.onUnexpectedError); + action.run(); action.dispose(); } private onIndentationClick(): void { const action = this.instantiationService.createInstance(ChangeIndentationAction, ChangeIndentationAction.ID, ChangeIndentationAction.LABEL); - action.run().done(null, errors.onUnexpectedError); + action.run(); action.dispose(); } @@ -524,14 +525,14 @@ export class EditorStatus implements IStatusbarItem { private onEOLClick(): void { const action = this.instantiationService.createInstance(ChangeEOLAction, ChangeEOLAction.ID, ChangeEOLAction.LABEL); - action.run().done(null, errors.onUnexpectedError); + action.run(); action.dispose(); } private onEncodingClick(): void { const action = this.instantiationService.createInstance(ChangeEncodingAction, ChangeEncodingAction.ID, ChangeEncodingAction.LABEL); - action.run().done(null, errors.onUnexpectedError); + action.run(); action.dispose(); } @@ -824,8 +825,8 @@ export class ShowLanguageExtensionsAction extends Action { export class ChangeModeAction extends Action { - public static readonly ID = 'workbench.action.editor.changeLanguageMode'; - public static readonly LABEL = nls.localize('changeMode', "Change Language Mode"); + static readonly ID = 'workbench.action.editor.changeLanguageMode'; + static readonly LABEL = nls.localize('changeMode', "Change Language Mode"); constructor( actionId: string, @@ -834,7 +835,7 @@ export class ChangeModeAction extends Action { @IModelService private modelService: IModelService, @IEditorService private editorService: IEditorService, @IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IPreferencesService private preferencesService: IPreferencesService, @IInstantiationService private instantiationService: IInstantiationService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService @@ -842,10 +843,10 @@ export class ChangeModeAction extends Action { super(actionId, actionLabel); } - public run(): TPromise { + run(): TPromise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { - return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); } const textModel = activeTextEditorWidget.getModel(); @@ -866,7 +867,7 @@ export class ChangeModeAction extends Action { // All languages are valid picks const languages = this.modeService.getRegisteredLanguageNames(); - const picks: IPickOpenEntry[] = languages.sort().map((lang, index) => { + const picks: QuickPickInput[] = languages.sort().map((lang, index) => { let description: string; if (currentModeId === lang) { description = nls.localize('languageDescription', "({0}) - Configured Language", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); @@ -886,20 +887,20 @@ export class ChangeModeAction extends Action { } } - return { + return { label: lang, - resource: fakeResource, + iconClasses: getIconClasses(this.modelService, this.modeService, fakeResource), description }; }); if (hasLanguageSupport) { - picks[0].separator = { border: true, label: nls.localize('languagesPicks', "languages (identifier)") }; + picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") }); } // Offer action to configure via settings - let configureModeAssociations: IPickOpenEntry; - let configureModeSettings: IPickOpenEntry; + let configureModeAssociations: IQuickPickItem; + let configureModeSettings: IQuickPickItem; let galleryAction: Action; if (hasLanguageSupport) { const ext = paths.extname(resource.fsPath) || paths.basename(resource.fsPath); @@ -916,7 +917,7 @@ export class ChangeModeAction extends Action { } // Offer to "Auto Detect" - const autoDetectMode: IPickOpenEntry = { + const autoDetectMode: IQuickPickItem = { label: nls.localize('autoDetect', "Auto Detect") }; @@ -924,7 +925,7 @@ export class ChangeModeAction extends Action { picks.unshift(autoDetectMode); } - return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }).then(pick => { + return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }).then(pick => { if (!pick) { return; } @@ -988,18 +989,18 @@ export class ChangeModeAction extends Action { const currentAssociation = this.modeService.getModeIdByFilenameOrFirstLine(basename); const languages = this.modeService.getRegisteredLanguageNames(); - const picks: IPickOpenEntry[] = languages.sort().map((lang, index) => { + const picks: IQuickPickItem[] = languages.sort().map((lang, index) => { const id = this.modeService.getModeIdForLanguageName(lang.toLowerCase()); - return { + return { id, label: lang, description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : void 0 }; }); - TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).done(() => { - this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || basename) }).done(language => { + setTimeout(() => { + this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || basename) }).then(language => { if (language) { const fileAssociationsConfig = this.configurationService.inspect(FILES_ASSOCIATIONS_CONFIG); @@ -1027,39 +1028,39 @@ export class ChangeModeAction extends Action { this.configurationService.updateValue(FILES_ASSOCIATIONS_CONFIG, currentAssociations, target); } }); - }); + }, 50 /* quick open is sensitive to being opened so soon after another */); } } -export interface IChangeEOLEntry extends IPickOpenEntry { +export interface IChangeEOLEntry extends IQuickPickItem { eol: EndOfLineSequence; } class ChangeIndentationAction extends Action { - public static readonly ID = 'workbench.action.editor.changeIndentation'; - public static readonly LABEL = nls.localize('changeIndentation', "Change Indentation"); + static readonly ID = 'workbench.action.editor.changeIndentation'; + static readonly LABEL = nls.localize('changeIndentation', "Change Indentation"); constructor( actionId: string, actionLabel: string, @IEditorService private editorService: IEditorService, - @IQuickOpenService private quickOpenService: IQuickOpenService + @IQuickInputService private quickInputService: IQuickInputService ) { super(actionId, actionLabel); } - public run(): TPromise { + run(): TPromise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { - return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); } if (!isWritableCodeEditor(activeTextEditorWidget)) { - return this.quickOpenService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } - const picks = [ + const picks: QuickPickInput[] = [ activeTextEditorWidget.getAction(IndentUsingSpaces.ID), activeTextEditorWidget.getAction(IndentUsingTabs.ID), activeTextEditorWidget.getAction(DetectIndentation.ID), @@ -1078,35 +1079,35 @@ class ChangeIndentationAction extends Action { }; }); - (picks[0]).separator = { label: nls.localize('indentView', "change view") }; - (picks[3]).separator = { label: nls.localize('indentConvert', "convert file"), border: true }; + picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") }); + picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") }); - return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }).then(action => action && action.run()); + return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }).then(action => action && action.run()); } } export class ChangeEOLAction extends Action { - public static readonly ID = 'workbench.action.editor.changeEOL'; - public static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence"); + static readonly ID = 'workbench.action.editor.changeEOL'; + static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence"); constructor( actionId: string, actionLabel: string, @IEditorService private editorService: IEditorService, - @IQuickOpenService private quickOpenService: IQuickOpenService + @IQuickInputService private quickInputService: IQuickInputService ) { super(actionId, actionLabel); } - public run(): TPromise { + run(): TPromise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { - return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); } if (!isWritableCodeEditor(activeTextEditorWidget)) { - return this.quickOpenService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } const textModel = activeTextEditorWidget.getModel(); @@ -1118,7 +1119,7 @@ export class ChangeEOLAction extends Action { const selectedIndex = (textModel && textModel.getEOL() === '\n') ? 0 : 1; - return this.quickOpenService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), autoFocus: { autoFocusIndex: selectedIndex } }).then(eol => { + return this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }).then(eol => { if (eol) { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorWidget); if (activeCodeEditor && isWritableCodeEditor(activeCodeEditor)) { @@ -1132,35 +1133,35 @@ export class ChangeEOLAction extends Action { export class ChangeEncodingAction extends Action { - public static readonly ID = 'workbench.action.editor.changeEncoding'; - public static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding"); + static readonly ID = 'workbench.action.editor.changeEncoding'; + static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding"); constructor( actionId: string, actionLabel: string, @IEditorService private editorService: IEditorService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService, @IFileService private fileService: IFileService ) { super(actionId, actionLabel); } - public run(): TPromise { + run(): TPromise { if (!getCodeEditor(this.editorService.activeTextEditorWidget)) { - return this.quickOpenService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); } let activeControl = this.editorService.activeControl; let encodingSupport: IEncodingSupport = toEditorWithEncodingSupport(activeControl.input); if (!encodingSupport) { - return this.quickOpenService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + return this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); } - let pickActionPromise: TPromise; + let pickActionPromise: TPromise; - let saveWithEncodingPick: IPickOpenEntry; - let reopenWithEncodingPick: IPickOpenEntry; + let saveWithEncodingPick: IQuickPickItem; + let reopenWithEncodingPick: IQuickPickItem; if (language === LANGUAGE_DEFAULT) { saveWithEncodingPick = { label: nls.localize('saveWithEncoding', "Save with Encoding") }; reopenWithEncodingPick = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") }; @@ -1174,7 +1175,7 @@ export class ChangeEncodingAction extends Action { } else if (!isWritableBaseEditor(activeControl)) { pickActionPromise = TPromise.as(reopenWithEncodingPick); } else { - pickActionPromise = this.quickOpenService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + pickActionPromise = this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); } return pickActionPromise.then(action => { @@ -1184,7 +1185,7 @@ export class ChangeEncodingAction extends Action { const resource = toResource(activeControl.input, { supportSideBySide: true }); - return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */) + return timeout(50 /* quick open is sensitive to being opened so soon after another */) .then(() => { if (!resource || !this.fileService.canHandleResource(resource)) { return TPromise.as(null); // encoding detection only possible for resources the file service can handle @@ -1201,7 +1202,7 @@ export class ChangeEncodingAction extends Action { let aliasMatchIndex: number; // All encodings are valid picks - const picks: IPickOpenEntry[] = Object.keys(SUPPORTED_ENCODINGS) + const picks: QuickPickInput[] = Object.keys(SUPPORTED_ENCODINGS) .sort((k1, k2) => { if (k1 === configuredEncoding) { return -1; @@ -1230,13 +1231,14 @@ export class ChangeEncodingAction extends Action { // If we have a guessed encoding, show it first unless it matches the configured encoding if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) { - picks[0].separator = { border: true }; + picks.unshift({ type: 'separator' }); picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") }); } - return this.quickOpenService.pick(picks, { + const items = picks.filter(p => p.type !== 'separator') as IQuickPickItem[]; + return this.quickInputService.pick(picks, { placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"), - autoFocus: { autoFocusIndex: typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : void 0 } + activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1] }).then(encoding => { if (encoding) { activeControl = this.editorService.activeControl; @@ -1264,7 +1266,7 @@ class ScreenReaderDetectedExplanation extends Themable { super(themeService); } - public get visible(): boolean { + get visible(): boolean { return this._visible; } @@ -1284,7 +1286,7 @@ class ScreenReaderDetectedExplanation extends Themable { } } - public show(anchorElement: HTMLElement): void { + show(anchorElement: HTMLElement): void { this._visible = true; this.contextViewService.showContextView({ @@ -1308,7 +1310,7 @@ class ScreenReaderDetectedExplanation extends Themable { }); } - public hide(): void { + hide(): void { this.contextViewService.hideContextView(); } diff --git a/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css new file mode 100644 index 00000000000..42d88269f09 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/breadcrumbscontrol.css @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.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 { + 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 { + text-decoration-line: underline; +} + +/* todo@joh move somewhere else */ + +.monaco-workbench .monaco-breadcrumbs-picker .picker-item { + line-height: 22px; + flex: 1; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree>.input { + padding: 5px 9px; + position: relative; + box-sizing: border-box; + height: 36px; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree>.tree { + height: calc(100% - 36px); +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree.inactive>.input { + display: none; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree.inactive>.tree { + height: 100%; +} + +.monaco-workbench .monaco-breadcrumbs-picker .highlighting-tree .monaco-highlighted-label .highlight{ + font-weight: bold; +} diff --git a/src/vs/workbench/browser/parts/editor/media/close-editor.svg b/src/vs/workbench/browser/parts/editor/media/close-big-alt.svg similarity index 100% rename from src/vs/workbench/browser/parts/editor/media/close-editor.svg rename to src/vs/workbench/browser/parts/editor/media/close-big-alt.svg diff --git a/src/vs/workbench/browser/parts/editor/media/close-editor-inverse.svg b/src/vs/workbench/browser/parts/editor/media/close-big-inverse-alt.svg similarity index 100% rename from src/vs/workbench/browser/parts/editor/media/close-editor-inverse.svg rename to src/vs/workbench/browser/parts/editor/media/close-big-inverse-alt.svg diff --git a/src/vs/workbench/browser/parts/editor/media/close-big-inverse.svg b/src/vs/workbench/browser/parts/editor/media/close-big-inverse.svg index ce0e5896405..a174033d912 100644 --- a/src/vs/workbench/browser/parts/editor/media/close-big-inverse.svg +++ b/src/vs/workbench/browser/parts/editor/media/close-big-inverse.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/close-big.svg b/src/vs/workbench/browser/parts/editor/media/close-big.svg index fde34404d4e..f4038b8bfa5 100644 --- a/src/vs/workbench/browser/parts/editor/media/close-big.svg +++ b/src/vs/workbench/browser/parts/editor/media/close-big.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/close-dirty-alt.svg b/src/vs/workbench/browser/parts/editor/media/close-dirty-alt.svg new file mode 100644 index 00000000000..409e5fa539c --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/close-dirty-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/close-dirty-inverse-alt.svg b/src/vs/workbench/browser/parts/editor/media/close-dirty-inverse-alt.svg new file mode 100644 index 00000000000..02dafab76fc --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/close-dirty-inverse-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/close-statusview-inverse.svg b/src/vs/workbench/browser/parts/editor/media/close-statusview-inverse.svg new file mode 100644 index 00000000000..ce0e5896405 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/close-statusview-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/close-statusview.svg b/src/vs/workbench/browser/parts/editor/media/close-statusview.svg new file mode 100644 index 00000000000..fde34404d4e --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/close-statusview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 99508ecaff5..37f00da2fdc 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -13,8 +13,9 @@ opacity: 0.5; /* dimmed to indicate inactive state */ } -.monaco-workbench > .part.editor > .content .editor-group-container.empty.active { - opacity: 1; /* indicate active group through undimmed state */ +.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 */ @@ -42,10 +43,28 @@ /* Title */ .monaco-workbench > .part.editor > .content .editor-group-container > .title { - height: 35px; + position: relative; display: flex; + flex-wrap: nowrap; box-sizing: border-box; overflow: hidden; + justify-content: space-between; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title.tabs { + flex-wrap: wrap; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title.title-border-bottom::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + z-index: 5; + pointer-events: none; + background-color: var(--title-border-bottom-color); + width: 100%; + height: 1px; } .monaco-workbench > .part.editor > .content .editor-group-container.empty > .title { @@ -73,12 +92,12 @@ } .vs .monaco-workbench > .part.editor > .content .editor-group-container > .editor-group-container-toolbar .close-editor-group { - background-image: url('close.svg'); + 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 { - background-image: url('close-inverse.svg'); + background-image: url('close-big-inverse.svg'); } /* Editor */ @@ -93,4 +112,9 @@ .monaco-workbench > .part.editor > .content .editor-group-container > .editor-container > .editor-instance { height: 100%; -} \ No newline at end of file +} + +.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 cc723196439..204d01c7f16 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorstatus.css +++ b/src/vs/workbench/browser/parts/editor/media/editorstatus.css @@ -70,10 +70,10 @@ } .monaco-shell.vs .screen-reader-detected-explanation .cancel { - background: url('close-big.svg') center center no-repeat; + 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 { - background: url('close-big-inverse.svg') center center no-repeat; + 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 7a2683b4f03..44e3897ded7 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -3,6 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench > .part.editor > .content .editor-group-container > .title > .label-container { + display: flex; + justify-content: flex-start; + align-items: center; + overflow: hidden; + flex: auto; +} + /* Title Label */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .title-label { @@ -13,17 +21,67 @@ padding-left: 20px; } +.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { + flex: none; +} + .monaco-workbench > .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ } +/* Breadcrumbs */ + +.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { + flex: 1 50%; + overflow: hidden; + padding: 0 6px; +} + +.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 { + font-style: italic; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { + content: '/'; + opacity: 1; + height: inherit; + width: inherit; + background-image: none; +} + +.monaco-workbench.windows > .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { + 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 { + /* 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 { + /* use dot separator for workspace folder */ + content: '•'; + padding: 0 4px; +} + +.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 { display: flex; flex: initial; opacity: 0.5; + height: 35px; } .monaco-workbench > .part.editor > .content .editor-group-container.active > .title .title-actions { opacity: 1; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index ffea31b5e53..020706de84f 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -32,6 +32,7 @@ /* Tab */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab { + position: relative; display: flex; white-space: nowrap; cursor: pointer; @@ -59,7 +60,7 @@ .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: ""; + 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) */ @@ -84,6 +85,37 @@ padding-right: 10px; } +/* 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 { + 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 { + display: block; + position: absolute; + top: 0; + left: 0; + z-index: 6; /* over possible title border */ + pointer-events: none; + background-color: var(--tab-border-top-color); + width: 100%; + height: 1px; +} + +.monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 6; /* over possible title border */ + pointer-events: none; + background-color: var(--tab-border-bottom-color); + width: 100%; + height: 1px; +} + /* Tab Label */ .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { @@ -96,7 +128,7 @@ } .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { - content: ""; + content: ''; position: absolute; right: 0; height: 100%; @@ -105,6 +137,10 @@ padding: 0; } +.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 { overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */ @@ -136,8 +172,9 @@ } .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 { - overflow: visible; /* ...but still show the close button on hover and when dirty */ +.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 { @@ -220,4 +257,35 @@ cursor: default; flex: initial; padding-left: 4px; -} \ No newline at end of file + height: 35px; +} + +/* Breadcrumbs */ + +.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 { + height: 22px; + line-height: 22px; +} + +.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 { + max-width: 80%; +} + +.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 { + 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 79479a2046f..41e78a153f2 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -11,11 +11,6 @@ flex: 1; } -.monaco-workbench > .part.editor > .content .editor-group-container.centered > .title .title-label { - flex-direction: row; - justify-content: center; -} - .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; @@ -26,7 +21,7 @@ .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 span, +.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; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 0d85263c31c..3fac0488955 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -11,15 +11,21 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, addClass, EventHelper, removeClass } from 'vs/base/browser/dom'; -import { IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { addDisposableListener, EventType, addClass, EventHelper, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { IEditorPartOptions, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { Color } from 'vs/base/common/color'; + +interface IRenderedEditorLabel { + editor: IEditorInput; + pinned: boolean; +} export class NoTabsTitleControl extends TitleControl { private titleContainer: HTMLElement; private editorLabel: ResourceLabel; - private lastRenderedActiveEditor: IEditorInput; + private activeLabel: IRenderedEditorLabel = Object.create(null); protected create(parent: HTMLElement): void { this.titleContainer = parent; @@ -31,10 +37,19 @@ export class NoTabsTitleControl extends TitleControl { // Gesture Support Gesture.addTarget(this.titleContainer); + const labelContainer = document.createElement('div'); + addClass(labelContainer, 'label-container'); + this.titleContainer.appendChild(labelContainer); + // Editor Label - this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, this.titleContainer, void 0)); + this.editorLabel = this._register(this.instantiationService.createInstance(ResourceLabel, labelContainer, void 0)); this._register(this.editorLabel.onClick(e => this.onTitleLabelClick(e))); + // Breadcrumbs + this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent }); + toggleClass(this.titleContainer, 'breadcrumbs', Boolean(this.breadcrumbsControl)); + this.toDispose.push({ dispose: () => removeClass(this.titleContainer, 'breadcrumbs') }); // import to remove because the container is a shared dom node + // Right Actions Container const actionsContainer = document.createElement('div'); addClass(actionsContainer, 'title-actions'); @@ -80,12 +95,21 @@ export class NoTabsTitleControl extends TitleControl { // Close editor on middle mouse click if (e instanceof MouseEvent && e.button === 1 /* Middle Button */) { + EventHelper.stop(e, true /* for https://github.com/Microsoft/vscode/issues/56715 */); + this.group.closeEditor(this.group.activeEditor); } } + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT; + } + openEditor(editor: IEditorInput): void { - this.ifActiveEditorChanged(() => this.redraw()); + const activeEditorChanged = this.ifActiveEditorChanged(() => this.redraw()); + if (!activeEditorChanged) { + this.ifActiveEditorPropertiesChanged(() => this.redraw()); + } } closeEditor(editor: IEditorInput): void { @@ -136,16 +160,36 @@ export class NoTabsTitleControl extends TitleControl { this.redraw(); } - private ifActiveEditorChanged(fn: () => void): void { + protected handleBreadcrumbsEnablementChange(): void { + toggleClass(this.titleContainer, 'breadcrumbs', Boolean(this.breadcrumbsControl)); + this.redraw(); + } + + private ifActiveEditorChanged(fn: () => void): boolean { if ( - !this.lastRenderedActiveEditor && this.group.activeEditor || // active editor changed from null => editor - this.lastRenderedActiveEditor && !this.group.activeEditor || // active editor changed from editor => null - !this.group.isActive(this.lastRenderedActiveEditor) // active editor changed from editorA => editorB + !this.activeLabel.editor && this.group.activeEditor || // active editor changed from null => editor + this.activeLabel.editor && !this.group.activeEditor || // active editor changed from editor => null + !this.group.isActive(this.activeLabel.editor) // active editor changed from editorA => editorB ) { fn(); + + return true; + } + + return false; + } + + private ifActiveEditorPropertiesChanged(fn: () => void): void { + if (!this.activeLabel.editor || !this.group.activeEditor) { + return; // need an active editor to check for properties changed + } + + if (this.activeLabel.pinned !== this.group.isPinned(this.group.activeEditor)) { + fn(); // only run if pinned state has changed } } + private ifEditorIsActive(editor: IEditorInput, fn: () => void): void { if (this.group.isActive(editor)) { fn(); // only run if editor is current active @@ -154,7 +198,21 @@ export class NoTabsTitleControl extends TitleControl { private redraw(): void { const editor = this.group.activeEditor; - this.lastRenderedActiveEditor = editor; + + const isEditorPinned = this.group.isPinned(this.group.activeEditor); + const isGroupActive = this.accessor.activeGroup === this.group; + + this.activeLabel = { editor, pinned: isEditorPinned }; + + // Update Breadcrumbs + if (this.breadcrumbsControl) { + if (isGroupActive) { + this.breadcrumbsControl.update(); + toggleClass(this.breadcrumbsControl.domNode, 'preview', !isEditorPinned); + } else { + this.breadcrumbsControl.hide(); + } + } // Clear if there is no editor if (!editor) { @@ -165,8 +223,6 @@ export class NoTabsTitleControl extends TitleControl { // Otherwise render it else { - const isEditorPinned = this.group.isPinned(this.group.activeEditor); - const isGroupActive = this.accessor.activeGroup === this.group; // Dirty state this.updateEditorDirty(editor); @@ -177,7 +233,9 @@ export class NoTabsTitleControl extends TitleControl { const { labelFormat } = this.accessor.partOptions; let description: string; - if (labelFormat === 'default' && !isGroupActive) { + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + description = ''; // hide description when showing breadcrumbs + } else if (labelFormat === 'default' && !isGroupActive) { description = ''; // hide description when group is not active and style is 'default' } else { description = editor.getDescription(this.getVerbosity(labelFormat)) || ''; @@ -188,7 +246,7 @@ export class NoTabsTitleControl extends TitleControl { title = ''; // dont repeat what is already shown } - this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['title-label'] }); + this.editorLabel.setLabel({ name, description, resource }, { title, italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'] }); if (isGroupActive) { this.editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND); } else { diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 6924bfb1f12..01b87948707 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; @@ -19,27 +19,29 @@ export interface IRangeHighlightDecoration { isWholeLine?: boolean; } -export class RangeHighlightDecorations implements IDisposable { +export class RangeHighlightDecorations extends Disposable { private rangeHighlightDecorationId: string = null; private editor: ICodeEditor = null; private editorDisposables: IDisposable[] = []; - private readonly _onHighlightRemoved: Emitter = new Emitter(); - public readonly onHighlghtRemoved: Event = this._onHighlightRemoved.event; + private readonly _onHighlightRemoved: Emitter = this._register(new Emitter()); + get onHighlghtRemoved(): Event { return this._onHighlightRemoved.event; } constructor(@IEditorService private editorService: IEditorService) { + super(); } - public removeHighlightRange() { + removeHighlightRange() { if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) { this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); this._onHighlightRemoved.fire(); } + this.rangeHighlightDecorationId = null; } - public highlightRange(range: IRangeHighlightDecoration, editor?: ICodeEditor) { + highlightRange(range: IRangeHighlightDecoration, editor?: ICodeEditor) { editor = editor ? editor : this.getEditor(range); if (editor) { this.doHighlightRange(editor, range); @@ -48,9 +50,11 @@ export class RangeHighlightDecorations implements IDisposable { private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { this.removeHighlightRange(); + editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); }); + this.setEditor(editor); } @@ -62,6 +66,7 @@ export class RangeHighlightDecorations implements IDisposable { return this.editorService.activeTextEditorWidget as ICodeEditor; } } + return null; } @@ -107,7 +112,9 @@ export class RangeHighlightDecorations implements IDisposable { return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); } - public dispose() { + dispose() { + super.dispose(); + if (this.editor && this.editor.getModel()) { this.removeHighlightRange(); this.disposeEditorListeners(); diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index b7962916769..298583135d3 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -6,17 +6,17 @@ import 'vs/css!./media/resourceviewer'; import * as nls from 'vs/nls'; import * as mimes from 'vs/base/common/mime'; -import URI from 'vs/base/common/uri'; -import { Builder, $ } from 'vs/base/browser/builder'; +import { URI } from 'vs/base/common/uri'; import * as DOM from 'vs/base/browser/dom'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; 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, StatusbarAlignment } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarItem, StatusbarItemDescriptor, IStatusbarRegistry, Extensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDisposable } from 'vs/base/common/lifecycle'; +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 { TPromise } from 'vs/base/common/winjs.base'; @@ -35,12 +35,12 @@ export interface IResourceDescriptor { } class BinarySize { - public static readonly KB = 1024; - public static readonly MB = BinarySize.KB * BinarySize.KB; - public static readonly GB = BinarySize.MB * BinarySize.KB; - public static readonly TB = BinarySize.GB * BinarySize.KB; + static readonly KB = 1024; + static readonly MB = BinarySize.KB * BinarySize.KB; + static readonly GB = BinarySize.MB * BinarySize.KB; + static readonly TB = BinarySize.GB * BinarySize.KB; - public static formatSize(size: number): string { + static formatSize(size: number): string { if (size < BinarySize.KB) { return nls.localize('sizeB', "{0}B", size); } @@ -61,8 +61,8 @@ class BinarySize { } } -export interface ResourceViewerContext { - layout(dimension: DOM.Dimension): void; +export interface ResourceViewerContext extends IDisposable { + layout?(dimension: DOM.Dimension): void; } /** @@ -73,7 +73,7 @@ export class ResourceViewer { private static readonly MAX_OPEN_INTERNAL_SIZE = BinarySize.MB * 200; // max size until we offer an action to open internally - public static show( + static show( descriptor: IResourceDescriptor, fileService: IFileService, container: HTMLElement, @@ -81,10 +81,10 @@ export class ResourceViewer { openInternalClb: (uri: URI) => void, openExternalClb: (uri: URI) => void, metadataClb: (meta: string) => void - ): ResourceViewerContext | null { + ): ResourceViewerContext { // Ensure CSS class - $(container).setClass('monaco-resource-viewer'); + container.className = 'monaco-resource-viewer'; // Images if (ResourceViewer.isImageResource(descriptor)) { @@ -93,21 +93,20 @@ export class ResourceViewer { // Large Files if (descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) { - FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb); + return FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb); } // Seemingly Binary Files else { - FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb); + return FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb); } - - return null; } private static isImageResource(descriptor: IResourceDescriptor) { const mime = getMime(descriptor); - return mime.indexOf('image/') >= 0; + // Chrome does not support tiffs + return mime.indexOf('image/') >= 0 && mime !== 'image/tiff'; } } @@ -115,21 +114,19 @@ class ImageView { private static readonly MAX_IMAGE_SIZE = BinarySize.MB; // showing images inline is memory intense, so we have a limit private static readonly BASE64_MARKER = 'base64,'; - public static create( + static create( container: HTMLElement, descriptor: IResourceDescriptor, fileService: IFileService, scrollbar: DomScrollableElement, openExternalClb: (uri: URI) => void, metadataClb: (meta: string) => void - ): ResourceViewerContext | null { + ): ResourceViewerContext { if (ImageView.shouldShowImageInline(descriptor)) { return InlineImageView.create(container, descriptor, fileService, scrollbar, metadataClb); } - LargeImageView.create(container, descriptor, openExternalClb); - - return null; + return LargeImageView.create(container, descriptor, openExternalClb); } private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean { @@ -153,76 +150,78 @@ class ImageView { } class LargeImageView { - public static create( + static create( container: HTMLElement, descriptor: IResourceDescriptor, openExternalClb: (uri: URI) => void ) { - const size = BinarySize.formatSize(descriptor.size); + DOM.clearNode(container); - const imageContainer = $(container) - .empty() - .p({ - text: nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size) - }); + const disposables: IDisposable[] = []; + + const label = document.createElement('p'); + label.textContent = nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", BinarySize.formatSize(descriptor.size)); + container.appendChild(label); if (descriptor.resource.scheme !== Schemas.data) { - imageContainer.append($('a', { - role: 'button', - class: 'embedded-link', - text: nls.localize('resourceOpenExternalButton', "Open image using external program?") - }).on(DOM.EventType.CLICK, (e) => { - openExternalClb(descriptor.resource); - })); + const link = DOM.append(label, DOM.$('a.embedded-link')); + link.setAttribute('role', 'button'); + link.textContent = nls.localize('resourceOpenExternalButton', "Open image using external program?"); + + disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternalClb(descriptor.resource))); } + + return combinedDisposable(disposables); } } class FileTooLargeFileView { - public static create( + static create( container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, metadataClb: (meta: string) => void ) { + DOM.clearNode(container); + const size = BinarySize.formatSize(descriptor.size); - $(container) - .empty() - .span({ - text: nls.localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size) - }); + const label = document.createElement('span'); + label.textContent = nls.localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size); + container.appendChild(label); if (metadataClb) { metadataClb(size); } scrollbar.scanDomNode(); + + return Disposable.None; } } class FileSeemsBinaryFileView { - public static create( + static create( container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, openInternalClb: (uri: URI) => void, metadataClb: (meta: string) => void ) { - const binaryContainer = $(container) - .empty() - .p({ - text: nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding.") - }); + DOM.clearNode(container); + + const disposables: IDisposable[] = []; + + const label = document.createElement('p'); + label.textContent = nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); + container.appendChild(label); if (descriptor.resource.scheme !== Schemas.data) { - binaryContainer.append($('a', { - role: 'button', - class: 'embedded-link', - text: nls.localize('openAsText', "Do you want to open it anyway?") - }).on(DOM.EventType.CLICK, (e) => { - openInternalClb(descriptor.resource); - })); + const link = DOM.append(label, DOM.$('a.embedded-link')); + link.setAttribute('role', 'button'); + link.textContent = nls.localize('openAsText', "Do you want to open it anyway?"); + + disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openInternalClb(descriptor.resource))); } if (metadataClb) { @@ -230,17 +229,20 @@ class FileSeemsBinaryFileView { } scrollbar.scanDomNode(); + + return combinedDisposable(disposables); } } type Scale = number | 'fit'; class ZoomStatusbarItem extends Themable implements IStatusbarItem { + + static instance: ZoomStatusbarItem; + showTimeout: number; - public static instance: ZoomStatusbarItem; private statusBarItem: HTMLElement; - private onSelectScale?: (scale: Scale) => void; constructor( @@ -249,8 +251,10 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { @IThemeService themeService: IThemeService ) { super(themeService); + ZoomStatusbarItem.instance = this; - this.toUnbind.push(editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); + + this._register(editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); } private onActiveEditorChanged(): void { @@ -258,7 +262,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { this.onSelectScale = void 0; } - public show(scale: Scale, onSelectScale: (scale: number) => void) { + show(scale: Scale, onSelectScale: (scale: number) => void) { clearTimeout(this.showTimeout); this.showTimeout = setTimeout(() => { this.onSelectScale = onSelectScale; @@ -267,22 +271,22 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { }, 0); } - public hide() { + hide() { this.statusBarItem.style.display = 'none'; } - public render(container: HTMLElement): IDisposable { + render(container: HTMLElement): IDisposable { if (!this.statusBarItem && container) { - this.statusBarItem = $(container).a() - .addClass('.zoom-statusbar-item') - .on('click', () => { - this.contextMenuService.showContextMenu({ - getAnchor: () => container, - getActions: () => TPromise.as(this.zoomActions) - }); - }) - .getHTMLElement(); + this.statusBarItem = DOM.append(container, DOM.$('a.zoom-statusbar-item')); + this.statusBarItem.setAttribute('role', 'button'); this.statusBarItem.style.display = 'none'; + + DOM.addDisposableListener(this.statusBarItem, DOM.EventType.CLICK, () => { + this.contextMenuService.showContextMenu({ + getAnchor: () => container, + getActions: () => TPromise.as(this.zoomActions) + }); + }); } return this; @@ -301,7 +305,7 @@ class ZoomStatusbarItem extends Themable implements IStatusbarItem { this.onSelectScale(scale); } - return null; + return void 0; })); } @@ -358,15 +362,18 @@ class InlineImageView { */ private static readonly imageStateCache = new LRUCache(100); - public static create( + static create( container: HTMLElement, descriptor: IResourceDescriptor, fileService: IFileService, scrollbar: DomScrollableElement, metadataClb: (meta: string) => void ) { - const context = { - layout(dimension: DOM.Dimension) { } + const disposables: IDisposable[] = []; + + const context: ResourceViewerContext = { + layout(dimension: DOM.Dimension) { }, + dispose: () => combinedDisposable(disposables).dispose() }; const cacheKey = descriptor.resource.toString(); @@ -376,41 +383,40 @@ class InlineImageView { const initialState: ImageState = InlineImageView.imageStateCache.get(cacheKey) || { scale: 'fit', offsetX: 0, offsetY: 0 }; let scale = initialState.scale; - let img: Builder | null = null; - let imgElement: HTMLImageElement | null = null; + let image: HTMLImageElement = null; function updateScale(newScale: Scale) { - if (!img || !imgElement.parentElement) { + if (!image || !image.parentElement) { return; } if (newScale === 'fit') { scale = 'fit'; - img.addClass('scale-to-fit'); - img.removeClass('pixelated'); - img.style('min-width', 'auto'); - img.style('width', 'auto'); + DOM.addClass(image, 'scale-to-fit'); + DOM.removeClass(image, 'pixelated'); + image.style.minWidth = 'auto'; + image.style.width = 'auto'; InlineImageView.imageStateCache.set(cacheKey, null); } else { - const oldWidth = imgElement.width; - const oldHeight = imgElement.height; + const oldWidth = image.width; + const oldHeight = image.height; scale = clamp(newScale, InlineImageView.MIN_SCALE, InlineImageView.MAX_SCALE); if (scale >= InlineImageView.PIXELATION_THRESHOLD) { - img.addClass('pixelated'); + DOM.addClass(image, 'pixelated'); } else { - img.removeClass('pixelated'); + DOM.removeClass(image, 'pixelated'); } - const { scrollTop, scrollLeft } = imgElement.parentElement; - const dx = (scrollLeft + imgElement.parentElement.clientWidth / 2) / imgElement.parentElement.scrollWidth; - const dy = (scrollTop + imgElement.parentElement.clientHeight / 2) / imgElement.parentElement.scrollHeight; + const { scrollTop, scrollLeft } = image.parentElement; + const dx = (scrollLeft + image.parentElement.clientWidth / 2) / image.parentElement.scrollWidth; + const dy = (scrollTop + image.parentElement.clientHeight / 2) / image.parentElement.scrollHeight; - img.removeClass('scale-to-fit'); - img.style('min-width', `${(imgElement.naturalWidth * scale)}px`); - img.style('width', `${(imgElement.naturalWidth * scale)}px`); + DOM.removeClass(image, 'scale-to-fit'); + image.style.minWidth = `${(image.naturalWidth * scale)}px`; + image.style.widows = `${(image.naturalWidth * scale)}px`; - const newWidth = imgElement.width; + const newWidth = image.width; const scaleFactor = (newWidth - oldWidth) / oldWidth; const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft); @@ -428,123 +434,126 @@ class InlineImageView { } function firstZoom() { - scale = imgElement.clientWidth / imgElement.naturalWidth; + scale = image.clientWidth / image.naturalWidth; updateScale(scale); } - $(container) - .on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => { - if (!img) { - return; - } - ctrlPressed = e.ctrlKey; - altPressed = e.altKey; + disposables.push(DOM.addDisposableListener(container, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + if (!image) { + return; + } + ctrlPressed = e.ctrlKey; + altPressed = e.altKey; - if (platform.isMacintosh ? altPressed : ctrlPressed) { - c.removeClass('zoom-in').addClass('zoom-out'); - } - }) - .on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => { - if (!img) { - return; - } + if (platform.isMacintosh ? altPressed : ctrlPressed) { + DOM.removeClass(container, 'zoom-in'); + DOM.addClass(container, 'zoom-out'); + } + })); - ctrlPressed = e.ctrlKey; - altPressed = e.altKey; + disposables.push(DOM.addDisposableListener(container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + if (!image) { + return; + } - if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { - c.removeClass('zoom-out').addClass('zoom-in'); - } - }) - .on(DOM.EventType.CLICK, (e: MouseEvent) => { - if (!img) { - return; - } + ctrlPressed = e.ctrlKey; + altPressed = e.altKey; - if (e.button !== 0) { - return; - } + if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { + DOM.removeClass(container, 'zoom-out'); + DOM.addClass(container, 'zoom-in'); + } + })); - // left click - if (scale === 'fit') { - firstZoom(); - } + disposables.push(DOM.addDisposableListener(container, DOM.EventType.CLICK, (e: MouseEvent) => { + if (!image) { + return; + } - if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in - let i = 0; - for (; i < InlineImageView.zoomLevels.length; ++i) { - if (InlineImageView.zoomLevels[i] > scale) { - break; - } + if (e.button !== 0) { + return; + } + + // left click + if (scale === 'fit') { + firstZoom(); + } + + if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in + let i = 0; + for (; i < InlineImageView.zoomLevels.length; ++i) { + if (InlineImageView.zoomLevels[i] > scale) { + break; } - updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE); - } else { - let i = InlineImageView.zoomLevels.length - 1; - for (; i >= 0; --i) { - if (InlineImageView.zoomLevels[i] < scale) { - break; - } + } + updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE); + } else { + let i = InlineImageView.zoomLevels.length - 1; + for (; i >= 0; --i) { + if (InlineImageView.zoomLevels[i] < scale) { + break; } - updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE); - } - }) - .on(DOM.EventType.WHEEL, (e: WheelEvent) => { - if (!img) { - return; } + updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE); + } + })); - const isScrollWhellKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed; - if (!isScrollWhellKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl - return; - } + disposables.push(DOM.addDisposableListener(container, DOM.EventType.WHEEL, (e: WheelEvent) => { + if (!image) { + return; + } - e.preventDefault(); - e.stopPropagation(); + const isScrollWhellKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed; + if (!isScrollWhellKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl + return; + } - if (scale === 'fit') { - firstZoom(); - } + e.preventDefault(); + e.stopPropagation(); - let delta = e.deltaY < 0 ? 1 : -1; + if (scale === 'fit') { + firstZoom(); + } - // Pinching should increase the scale - if (e.ctrlKey && !isScrollWhellKeyPressed) { - delta *= -1; - } - updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR)); - }) - .on(DOM.EventType.SCROLL, () => { - if (!imgElement || !imgElement.parentElement || scale === 'fit') { - return; - } + let delta = e.deltaY < 0 ? 1 : -1; - const entry = InlineImageView.imageStateCache.get(cacheKey); - if (entry) { - const { scrollTop, scrollLeft } = imgElement.parentElement; - InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop }); - } - }); + // Pinching should increase the scale + if (e.ctrlKey && !isScrollWhellKeyPressed) { + delta *= -1; + } + updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR)); + })); - $(container) - .empty() - .addClass('image', 'zoom-in') - .img({}) - .style('visibility', 'hidden') - .addClass('scale-to-fit') - .on(DOM.EventType.LOAD, (e, i) => { - img = i; - imgElement = img.getHTMLElement() as HTMLImageElement; - metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', imgElement.naturalWidth, imgElement.naturalHeight, BinarySize.formatSize(descriptor.size))); - scrollbar.scanDomNode(); - img.style('visibility', 'visible'); - updateScale(scale); - if (initialState.scale !== 'fit') { - scrollbar.setScrollPosition({ - scrollLeft: initialState.offsetX, - scrollTop: initialState.offsetY, - }); - } - }); + disposables.push(DOM.addDisposableListener(container, DOM.EventType.SCROLL, () => { + if (!image || !image.parentElement || scale === 'fit') { + return; + } + + const entry = InlineImageView.imageStateCache.get(cacheKey); + if (entry) { + const { scrollTop, scrollLeft } = image.parentElement; + InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop }); + } + })); + + DOM.clearNode(container); + DOM.addClasses(container, 'image', 'zoom-in'); + + image = DOM.append(container, DOM.$('img.scale-to-fit')); + image.style.visibility = 'hidden'; + + disposables.push(DOM.addDisposableListener(image, DOM.EventType.LOAD, e => { + metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size))); + scrollbar.scanDomNode(); + image.style.visibility = 'visible'; + updateScale(scale); + if (initialState.scale !== 'fit') { + scrollbar.setScrollPosition({ + scrollLeft: initialState.offsetX, + scrollTop: initialState.offsetY, + }); + } + })); InlineImageView.imageSrc(descriptor, fileService).then(dataUri => { const imgs = container.getElementsByTagName('img'); @@ -563,6 +572,7 @@ class InlineImageView { return fileService.resolveContent(descriptor.resource, { encoding: 'base64' }).then(data => { const mime = getMime(descriptor); + return `data:${mime};base64,${data.value}`; }); } @@ -571,7 +581,7 @@ class InlineImageView { function getMime(descriptor: IResourceDescriptor) { let mime = descriptor.mime; if (!mime && descriptor.resource.scheme !== Schemas.data) { - mime = mimes.getMediaMime(descriptor.resource.toString()); + mime = mimes.getMediaMime(descriptor.resource.path); } return mime || mimes.MIME_BINARY; } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index beaeeb75ed3..b38e32f681a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -8,7 +8,6 @@ import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditor } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { VSash } from 'vs/base/browser/ui/sash/sash'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -16,20 +15,46 @@ import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { SplitView, Sizing, Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { Event, Relay, anyEvent, mapEvent, Emitter } from 'vs/base/common/event'; export class SideBySideEditor extends BaseEditor { - public static readonly ID: string = 'workbench.editor.sidebysideEditor'; + static readonly ID: string = 'workbench.editor.sidebysideEditor'; - private dimension: DOM.Dimension; + get minimumMasterWidth() { return this.masterEditor ? this.masterEditor.minimumWidth : 0; } + get maximumMasterWidth() { return this.masterEditor ? this.masterEditor.maximumWidth : Number.POSITIVE_INFINITY; } + get minimumMasterHeight() { return this.masterEditor ? this.masterEditor.minimumHeight : 0; } + get maximumMasterHeight() { return this.masterEditor ? this.masterEditor.maximumHeight : Number.POSITIVE_INFINITY; } + + get minimumDetailsWidth() { return this.detailsEditor ? this.detailsEditor.minimumWidth : 0; } + get maximumDetailsWidth() { return this.detailsEditor ? this.detailsEditor.maximumWidth : Number.POSITIVE_INFINITY; } + get minimumDetailsHeight() { return this.detailsEditor ? this.detailsEditor.minimumHeight : 0; } + get maximumDetailsHeight() { return this.detailsEditor ? this.detailsEditor.maximumHeight : Number.POSITIVE_INFINITY; } + + // these setters need to exist because this extends from BaseEditor + set minimumWidth(value: number) { /* noop */ } + set maximumWidth(value: number) { /* noop */ } + set minimumHeight(value: number) { /* noop */ } + set maximumHeight(value: number) { /* noop */ } + + get minimumWidth() { return this.minimumMasterWidth + this.minimumDetailsWidth; } + get maximumWidth() { return this.maximumMasterWidth + this.maximumDetailsWidth; } + get minimumHeight() { return this.minimumMasterHeight + this.minimumDetailsHeight; } + get maximumHeight() { return this.maximumMasterHeight + this.maximumDetailsHeight; } protected masterEditor: BaseEditor; - private masterEditorContainer: HTMLElement; - protected detailsEditor: BaseEditor; + + private masterEditorContainer: HTMLElement; private detailsEditorContainer: HTMLElement; - private sash: VSash; + private splitview: SplitView; + private dimension: DOM.Dimension = new DOM.Dimension(0, 0); + + private onDidCreateEditors = this._register(new Emitter<{ width: number; height: number; }>()); + private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; }>()); + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = anyEvent(this.onDidCreateEditors.event, this._onDidSizeConstraintsChange.event); constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -41,16 +66,38 @@ export class SideBySideEditor extends BaseEditor { protected createEditor(parent: HTMLElement): void { DOM.addClass(parent, 'side-by-side-editor'); - this.createSash(parent); + + this.splitview = this._register(new SplitView(parent, { orientation: Orientation.HORIZONTAL })); + this._register(this.splitview.onDidSashReset(() => this.splitview.distributeViewSizes())); + + this.detailsEditorContainer = DOM.$('.details-editor-container'); + this.splitview.addView({ + element: this.detailsEditorContainer, + layout: size => this.detailsEditor && this.detailsEditor.layout(new DOM.Dimension(size, this.dimension.height)), + minimumSize: 220, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.None + }, Sizing.Distribute); + + this.masterEditorContainer = DOM.$('.master-editor-container'); + this.splitview.addView({ + element: this.masterEditorContainer, + layout: size => this.masterEditor && this.masterEditor.layout(new DOM.Dimension(size, this.dimension.height)), + minimumSize: 220, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.None + }, Sizing.Distribute); + + this.updateStyles(); } - public setInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Thenable { const oldInput = this.input; return super.setInput(newInput, options, token) .then(() => this.updateInput(oldInput, newInput, options, token)); } - public setOptions(options: EditorOptions): void { + setOptions(options: EditorOptions): void { if (this.masterEditor) { this.masterEditor.setOptions(options); } @@ -66,7 +113,7 @@ export class SideBySideEditor extends BaseEditor { super.setEditorVisible(visible, group); } - public clearInput(): void { + clearInput(): void { if (this.masterEditor) { this.masterEditor.clearInput(); } @@ -77,57 +124,49 @@ export class SideBySideEditor extends BaseEditor { super.clearInput(); } - public focus(): void { + focus(): void { if (this.masterEditor) { this.masterEditor.focus(); } } - public layout(dimension: DOM.Dimension): void { + layout(dimension: DOM.Dimension): void { this.dimension = dimension; - this.sash.setDimenesion(this.dimension); + this.splitview.layout(dimension.width); } - public getControl(): IEditorControl { + getControl(): IEditorControl { if (this.masterEditor) { return this.masterEditor.getControl(); } return null; } - public getMasterEditor(): IEditor { + getMasterEditor(): IEditor { return this.masterEditor; } - public getDetailsEditor(): IEditor { + getDetailsEditor(): IEditor { return this.detailsEditor; } - public supportsCenteredLayout(): boolean { - return false; - } - - private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): void { + private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Thenable { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); } - this.createEditorContainers(); return this.setNewInput(newInput, options, token); - } else { - this.detailsEditor.setInput(newInput.details, null, token); - this.masterEditor.setInput(newInput.master, options, token); - - return void 0; } + + return TPromise.join([this.detailsEditor.setInput(newInput.details, null, token), this.masterEditor.setInput(newInput.master, options, token)]).then(() => void 0); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): void { + private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Thenable { const detailsEditor = this._createEditor(newInput.details, this.detailsEditorContainer); const masterEditor = this._createEditor(newInput.master, this.masterEditorContainer); - this.onEditorsCreated(detailsEditor, masterEditor, newInput.details, newInput.master, options, token); + return this.onEditorsCreated(detailsEditor, masterEditor, newInput.details, newInput.master, options, token); } private _createEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { @@ -143,21 +182,18 @@ export class SideBySideEditor extends BaseEditor { private onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions, token: CancellationToken): TPromise { this.detailsEditor = details; this.masterEditor = master; - this.dolayout(this.sash.getVerticalSashLeft()); + + this._onDidSizeConstraintsChange.input = anyEvent( + mapEvent(details.onDidSizeConstraintsChange, () => undefined), + mapEvent(master.onDidSizeConstraintsChange, () => undefined) + ); + + this.onDidCreateEditors.fire(); + return TPromise.join([this.detailsEditor.setInput(detailsInput, null, token), this.masterEditor.setInput(masterInput, options, token)]).then(() => this.focus()); } - private createEditorContainers(): void { - const parentElement = this.getContainer(); - this.detailsEditorContainer = DOM.append(parentElement, DOM.$('.details-editor-container')); - this.detailsEditorContainer.style.position = 'absolute'; - this.masterEditorContainer = DOM.append(parentElement, DOM.$('.master-editor-container')); - this.masterEditorContainer.style.position = 'absolute'; - - this.updateStyles(); - } - - public updateStyles(): void { + updateStyles(): void { super.updateStyles(); if (this.masterEditorContainer) { @@ -165,52 +201,24 @@ export class SideBySideEditor extends BaseEditor { } } - private createSash(parentElement: HTMLElement): void { - this.sash = this._register(new VSash(parentElement, 220)); - this._register(this.sash.onPositionChange(position => this.dolayout(position))); - } - - private dolayout(splitPoint: number): void { - if (!this.detailsEditor || !this.masterEditor || !this.dimension) { - return; - } - const masterEditorWidth = this.dimension.width - splitPoint; - const detailsEditorWidth = this.dimension.width - masterEditorWidth; - - this.detailsEditorContainer.style.width = `${detailsEditorWidth}px`; - this.detailsEditorContainer.style.height = `${this.dimension.height}px`; - this.detailsEditorContainer.style.left = '0px'; - - this.masterEditorContainer.style.width = `${masterEditorWidth}px`; - this.masterEditorContainer.style.height = `${this.dimension.height}px`; - this.masterEditorContainer.style.left = `${splitPoint}px`; - - this.detailsEditor.layout(new DOM.Dimension(detailsEditorWidth, this.dimension.height)); - this.masterEditor.layout(new DOM.Dimension(masterEditorWidth, this.dimension.height)); - } - private disposeEditors(): void { - const parentContainer = this.getContainer(); if (this.detailsEditor) { this.detailsEditor.dispose(); this.detailsEditor = null; } + if (this.masterEditor) { this.masterEditor.dispose(); this.masterEditor = null; } - if (this.detailsEditorContainer) { - parentContainer.removeChild(this.detailsEditorContainer); - this.detailsEditorContainer = null; - } - if (this.masterEditorContainer) { - parentContainer.removeChild(this.masterEditorContainer); - this.masterEditorContainer = null; - } + + this.detailsEditorContainer.innerHTML = ''; + this.masterEditorContainer.innerHTML = ''; } - public dispose(): void { + dispose(): void { this.disposeEditors(); + super.dispose(); } } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 5356db9ba28..5893cb53424 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -27,8 +27,8 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme'; -import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP } from 'vs/workbench/common/theme'; +import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, fillResourceDataTransfers, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -39,6 +39,8 @@ import { addClass, addDisposableListener, hasClass, EventType, EventHelper, remo import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; interface IEditorInputLabel { name: string; @@ -78,9 +80,10 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickOpenService quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @IConfigurationService configurationService: IConfigurationService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService); } protected create(parent: HTMLElement): void { @@ -108,6 +111,12 @@ export class TabsTitleControl extends TitleControl { // Close Action this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + + // Breadcrumbs + const breadcrumbsContainer = document.createElement('div'); + addClass(breadcrumbsContainer, 'tabs-breadcrumbs'); + this.titleContainer.appendChild(breadcrumbsContainer); + this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: breadcrumbsBackground }); } private createScrollbar(): void { @@ -128,6 +137,19 @@ export class TabsTitleControl extends TitleControl { this.titleContainer.appendChild(this.scrollbar.getDomNode()); } + private updateBreadcrumbsControl(): void { + if (this.breadcrumbsControl && this.breadcrumbsControl.update()) { + // relayout when we have a breadcrumbs and when update changed + // its hidden-status + this.group.relayout(); + } + } + + protected handleBreadcrumbsEnablementChange(): void { + // relayout when breadcrumbs are enable/disabled + this.group.relayout(); + } + private registerContainerListeners(): void { // Group dragging @@ -168,6 +190,7 @@ export class TabsTitleControl extends TitleControl { // Return if the target is not on the tabs container if (e.target !== this.tabsContainer) { + this.updateDropFeedback(this.tabsContainer, false); // fixes https://github.com/Microsoft/vscode/issues/52093 return; } @@ -239,6 +262,9 @@ export class TabsTitleControl extends TitleControl { // Redraw all tabs this.redraw(); + + // Update Breadcrumbs + this.updateBreadcrumbsControl(); } closeEditor(editor: IEditorInput): void { @@ -286,6 +312,9 @@ export class TabsTitleControl extends TitleControl { this.clearEditorActionsToolbar(); } + + // Update Breadcrumbs + this.updateBreadcrumbsControl(); } moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void { @@ -382,6 +411,11 @@ export class TabsTitleControl extends TitleControl { // Gesture Support Gesture.addTarget(tabContainer); + // Tab Border Top + const tabBorderTopContainer = document.createElement('div'); + addClass(tabBorderTopContainer, 'tab-border-top-container'); + tabContainer.appendChild(tabBorderTopContainer); + // Tab Editor Label const editorLabel = this.instantiationService.createInstance(ResourceLabel, tabContainer, void 0); this.tabLabelWidgets.push(editorLabel); @@ -391,6 +425,11 @@ export class TabsTitleControl extends TitleControl { addClass(tabCloseContainer, 'tab-close'); tabContainer.appendChild(tabCloseContainer); + // Tab Border Bottom + const tabBorderBottomContainer = document.createElement('div'); + addClass(tabBorderBottomContainer, 'tab-border-bottom-container'); + tabContainer.appendChild(tabBorderBottomContainer); + const tabActionRunner = new EditorCommandsContextActionRunner({ groupId: this.group.id, editorIndex: index }); const tabActionBar = new ActionBar(tabCloseContainer, { ariaLabel: localize('araLabelTabActions', "Tab actions"), actionRunner: tabActionRunner }); @@ -451,6 +490,8 @@ export class TabsTitleControl extends TitleControl { tab.blur(); if (e.button === 1 /* Middle Button*/ && !this.originatesFromTabActionBar(e)) { + e.stopPropagation(); // for https://github.com/Microsoft/vscode/issues/56715 + this.blockRevealActiveTabOnce(); this.closeOneEditorAction.run({ groupId: this.group.id, editorIndex: index }); } @@ -830,19 +871,22 @@ export class TabsTitleControl extends TitleControl { tabContainer.setAttribute('aria-selected', 'true'); tabContainer.style.backgroundColor = this.getColor(TAB_ACTIVE_BACKGROUND); - const activeTabBorderColor = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); - const activeTabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); - if (activeTabBorderColor) { - // Use boxShadow for the active tab border because if we also have a editor group header - // color, the two colors would collide and the tab border never shows up. - // see https://github.com/Microsoft/vscode/issues/33111 - // In case of tabs container having a border, we need to inset -2px for the border to show up. - const hasTabsContainerBorder = !!this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER); - tabContainer.style.boxShadow = `${activeTabBorderColor} 0 ${hasTabsContainerBorder ? -2 : -1}px inset`; - } else if (activeTabBorderColorTop) { - tabContainer.style.boxShadow = `${activeTabBorderColorTop} 0 2px inset`; + const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); + if (activeTabBorderColorBottom) { + addClass(tabContainer, 'tab-border-bottom'); + tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom.toString()); } else { - tabContainer.style.boxShadow = null; + removeClass(tabContainer, 'tab-border-bottom'); + tabContainer.style.removeProperty('--tab-border-bottom-color'); + } + + const activeTabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); + if (activeTabBorderColorTop) { + addClass(tabContainer, 'tab-border-top'); + tabContainer.style.setProperty('--tab-border-top-color', activeTabBorderColorTop.toString()); + } else { + removeClass(tabContainer, 'tab-border-top'); + tabContainer.style.removeProperty('--tab-border-top-color'); } // Label @@ -896,6 +940,11 @@ export class TabsTitleControl extends TitleControl { return; } + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); + this.scrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + } + const visibleContainerWidth = this.tabsContainer.offsetWidth; const totalContainerWidth = this.tabsContainer.scrollWidth; @@ -1115,13 +1164,13 @@ 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 > .title.active .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { - background: linear-gradient(to left, ${adjustedColor}, transparent); + .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 > .title.active .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { - background: linear-gradient(to left, ${adjustedColorDrag}, transparent); + .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; } `); } @@ -1132,11 +1181,11 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { 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 { - background: linear-gradient(to left, ${adjustedColor}, transparent); + 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 { - background: linear-gradient(to left, ${adjustedColorDrag}, transparent); + background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); } @@ -1147,7 +1196,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { 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 > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after { - background: linear-gradient(to left, ${adjustedColorDrag}, transparent); + background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); } @@ -1174,7 +1223,6 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const adjustedColor = tabInactiveBackground.flatten(adjustedTabBackground); const adjustedColorDrag = tabInactiveBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench > .part.editor > .content .editor-group-container > .title .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); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 33b9d3da093..bb7224c8951 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -14,7 +14,7 @@ import * as types from 'vs/base/common/types'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditor } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditor, IEditorMemento } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; @@ -31,21 +31,23 @@ import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/co import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { once } from 'vs/base/common/event'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { IWindowService } from 'vs/platform/windows/common/windows'; /** * The text editor that leverages the diff text editor for the editing experience. */ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { - public static readonly ID = TEXT_DIFF_EDITOR_ID; + static readonly ID = TEXT_DIFF_EDITOR_ID; private diffNavigator: DiffNavigator; - private diffNavigatorDisposables: IDisposable[]; + private diffNavigatorDisposables: IDisposable[] = []; private nextDiffAction: NavigateAction; private previousDiffAction: NavigateAction; private toggleIgnoreTrimWhitespaceAction: ToggleIgnoreTrimWhitespaceAction; @@ -60,22 +62,22 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService textFileService: ITextFileService, + @IWindowService windowService: IWindowService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService); + super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService); - this.diffNavigatorDisposables = []; - this.toUnbind.push(this._actualConfigurationService.onDidChangeConfiguration((e) => { + this._register(this._actualConfigurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('diffEditor.ignoreTrimWhitespace')) { this.updateIgnoreTrimWhitespaceAction(); } })); } - protected getEditorViewStateStorage(): object { - return Object.create(null); // do not persist in storage as diff editors are never persisted + protected getEditorMemento(storageService: IStorageService, editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + return new EditorMemento(this.getId(), key, Object.create(null), limit, editorGroupService); // do not persist in storage as diff editors are never persisted } - public getTitle(): string { + getTitle(): string { if (this.input) { return this.input.getName(); } @@ -83,7 +85,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return nls.localize('textDiffEditor', "Text Diff Editor"); } - public createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { + createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { // Actions this.nextDiffAction = new NavigateAction(this, true); @@ -94,7 +96,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } - public setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { // Dispose previous diff navigator this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); @@ -104,7 +106,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(resolvedModel => { + return input.resolve().then(resolvedModel => { // Check for cancellation if (token.isCancellationRequested) { @@ -118,7 +120,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { // Set Editor Model const diffEditor = this.getControl(); - diffEditor.setModel((resolvedModel).textDiffEditorModel); + const resolvedDiffEditorModel = resolvedModel; + diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); // Apply Options from TextOptions let optionsGotApplied = false; @@ -132,6 +135,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { hasPreviousViewState = this.restoreTextDiffEditorViewState(input); } + // Diff navigator this.diffNavigator = new DiffNavigator(diffEditor, { alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState // only reveal first change if we had no options or viewstate }); @@ -142,7 +146,11 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { this.previousDiffAction.updateEnablement(); })); + // Enablement of actions this.updateIgnoreTrimWhitespaceAction(); + + // Readonly flag + diffEditor.updateOptions({ readOnly: resolvedDiffEditorModel.isReadonly() }); }, error => { // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. @@ -156,17 +164,13 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { }); } - public setOptions(options: EditorOptions): void { + setOptions(options: EditorOptions): void { const textOptions = options; if (textOptions && types.isFunction(textOptions.apply)) { textOptions.apply(this.getControl(), ScrollType.Smooth); } } - public supportsCenteredLayout(): boolean { - return false; - } - private restoreTextDiffEditorViewState(input: EditorInput): boolean { if (input instanceof DiffEditorInput) { const resource = this.toDiffEditorViewStateResource(input); @@ -230,6 +234,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { const options: IDiffEditorOptions = super.getConfigurationOverrides(); options.readOnly = this.isReadOnly(); + options.lineDecorationsWidth = '2ch'; return options; } @@ -268,7 +273,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return (error).fileOperationResult === FileOperationResult.FILE_IS_BINARY; } - public clearInput(): void { + clearInput(): void { // Dispose previous diff navigator this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); @@ -283,11 +288,11 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { super.clearInput(); } - public getDiffNavigator(): DiffNavigator { + getDiffNavigator(): DiffNavigator { return this.diffNavigator; } - public getActions(): IAction[] { + getActions(): IAction[] { return [ this.toggleIgnoreTrimWhitespaceAction, this.previousDiffAction, @@ -295,7 +300,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { ]; } - public getControl(): IDiffEditor { + getControl(): IDiffEditor { return super.getControl() as IDiffEditor; } @@ -372,7 +377,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return URI.from({ scheme: 'diff', path: `${btoa(original.toString())}${btoa(modified.toString())}` }); } - public dispose(): void { + dispose(): void { this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); super.dispose(); @@ -397,7 +402,7 @@ class NavigateAction extends Action { this.enabled = false; } - public run(): TPromise { + run(): TPromise { if (this.next) { this.editor.getDiffNavigator().next(); } else { @@ -407,7 +412,7 @@ class NavigateAction extends Action { return null; } - public updateEnablement(): void { + updateEnablement(): void { this.enabled = this.editor.getDiffNavigator().canNavigate(); } } @@ -424,12 +429,12 @@ class ToggleIgnoreTrimWhitespaceAction extends Action { this.label = nls.localize('toggleIgnoreTrimWhitespace.label', "Ignore Trim Whitespace"); } - public updateClassName(ignoreTrimWhitespace: boolean): void { + updateClassName(ignoreTrimWhitespace: boolean): void { this._isChecked = ignoreTrimWhitespace; this.class = `textdiff-editor-action toggleIgnoreTrimWhitespace${this._isChecked ? ' is-checked' : ''}`; } - public run(): TPromise { + run(): TPromise { this._configurationService.updateValue(`diffEditor.ignoreTrimWhitespace`, !this._isChecked); return null; } diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 16ba8b23f83..e47bdd9ac16 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -6,20 +6,18 @@ 'use strict'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as objects from 'vs/base/common/objects'; import * as types from 'vs/base/common/types'; -import * as errors from 'vs/base/common/errors'; import * as DOM from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, EditorViewStateMemento, ITextEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditor } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorViewState, IEditor } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Scope } from 'vs/workbench/common/memento'; import { ITextFileService, SaveReason, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -27,6 +25,7 @@ import { isDiffEditor, isCodeEditor, ICodeEditor, getCodeEditor } from 'vs/edito import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWindowService } from 'vs/platform/windows/common/windows'; const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; @@ -44,28 +43,25 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { private _editorContainer: HTMLElement; private hasPendingConfigurationChange: boolean; private lastAppliedEditorOptions: IEditorOptions; - private editorViewStateMemento: EditorViewStateMemento; + private editorMemento: IEditorMemento; constructor( id: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IStorageService private storageService: IStorageService, + @IStorageService storageService: IStorageService, @ITextResourceConfigurationService private readonly _configurationService: ITextResourceConfigurationService, @IThemeService protected themeService: IThemeService, @ITextFileService private readonly _textFileService: ITextFileService, @IEditorService protected editorService: IEditorService, @IEditorGroupsService protected editorGroupService: IEditorGroupsService, + @IWindowService private windowService: IWindowService ) { super(id, telemetryService, themeService); - this.editorViewStateMemento = new EditorViewStateMemento(editorGroupService, this.getEditorViewStateStorage(), TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); + this.editorMemento = this.getEditorMemento(storageService, editorGroupService, TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getValue(this.getResource())))); - } - - protected getEditorViewStateStorage(): object { - return this.getMemento(this.storageService, Scope.WORKSPACE); + this._register(this.configurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(this.configurationService.getValue(this.getResource())))); } protected get instantiationService(): IInstantiationService { @@ -135,33 +131,35 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { // Editor for Text this._editorContainer = parent; - this.editorControl = this.createEditorControl(parent, this.computeConfiguration(this.configurationService.getValue(this.getResource()))); + this.editorControl = this._register(this.createEditorControl(parent, this.computeConfiguration(this.configurationService.getValue(this.getResource())))); // Model & Language changes const codeEditor = getCodeEditor(this.editorControl); if (codeEditor) { - this.toUnbind.push(codeEditor.onDidChangeModelLanguage(e => this.updateEditorConfiguration())); - this.toUnbind.push(codeEditor.onDidChangeModel(e => this.updateEditorConfiguration())); + this._register(codeEditor.onDidChangeModelLanguage(e => this.updateEditorConfiguration())); + this._register(codeEditor.onDidChangeModel(e => this.updateEditorConfiguration())); } // Application & Editor focus change to respect auto save settings if (isCodeEditor(this.editorControl)) { - this.toUnbind.push(this.editorControl.onDidBlurEditorWidget(() => this.onEditorFocusLost())); + this._register(this.editorControl.onDidBlurEditorWidget(() => this.onEditorFocusLost())); } else if (isDiffEditor(this.editorControl)) { - this.toUnbind.push(this.editorControl.getOriginalEditor().onDidBlurEditorWidget(() => this.onEditorFocusLost())); - this.toUnbind.push(this.editorControl.getModifiedEditor().onDidBlurEditorWidget(() => this.onEditorFocusLost())); + this._register(this.editorControl.getOriginalEditor().onDidBlurEditorWidget(() => this.onEditorFocusLost())); + this._register(this.editorControl.getModifiedEditor().onDidBlurEditorWidget(() => this.onEditorFocusLost())); } - this.toUnbind.push(this.editorService.onDidActiveEditorChange(() => this.onEditorFocusLost())); - this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onWindowFocusLost())); + this._register(this.editorService.onDidActiveEditorChange(() => this.onEditorFocusLost())); + this._register(this.windowService.onDidChangeFocus(focused => this.onWindowFocusChange(focused))); } private onEditorFocusLost(): void { this.maybeTriggerSaveAll(SaveReason.FOCUS_CHANGE); } - private onWindowFocusLost(): void { - this.maybeTriggerSaveAll(SaveReason.WINDOW_CHANGE); + private onWindowFocusChange(focused: boolean): void { + if (!focused) { + this.maybeTriggerSaveAll(SaveReason.WINDOW_CHANGE); + } } private maybeTriggerSaveAll(reason: SaveReason): void { @@ -174,7 +172,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { (reason === SaveReason.FOCUS_CHANGE && mode === AutoSaveMode.ON_FOCUS_CHANGE) ) { if (this.textFileService.isDirty()) { - this.textFileService.saveAll(void 0, { reason }).done(null, errors.onUnexpectedError); + this.textFileService.saveAll(void 0, { reason }); } } } @@ -191,7 +189,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - public setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { return super.setInput(input, options, token).then(() => { // Update editor options after having set the input. We do this because there can be @@ -214,17 +212,17 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { super.setEditorVisible(visible, group); } - public focus(): void { + focus(): void { this.editorControl.focus(); } - public layout(dimension: DOM.Dimension): void { + layout(dimension: DOM.Dimension): void { // Pass on to Editor this.editorControl.layout(dimension); } - public getControl(): IEditor { + getControl(): IEditor { return this.editorControl; } @@ -237,7 +235,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { return; } - this.editorViewStateMemento.saveState(this.group, resource, editorViewState); + this.editorMemento.saveState(this.group, resource, editorViewState); } protected retrieveTextEditorViewState(resource: URI): IEditorViewState { @@ -264,7 +262,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { */ protected clearTextEditorViewState(resources: URI[]): void { resources.forEach(resource => { - this.editorViewStateMemento.clearState(resource); + this.editorMemento.clearState(resource); }); } @@ -272,7 +270,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { * Loads the text editor view state for the given resource and returns it. */ protected loadTextEditorViewState(resource: URI): IEditorViewState { - return this.editorViewStateMemento.loadState(this.group, resource); + return this.editorMemento.loadState(this.group, resource); } private updateEditorConfiguration(configuration = this.configurationService.getValue(this.getResource())): void { @@ -314,17 +312,8 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { protected abstract getAriaLabel(): string; - protected saveMemento(): void { - - // ensure to first save our view state memento - this.editorViewStateMemento.save(); - - super.saveMemento(); - } - - public dispose(): void { + dispose(): void { this.lastAppliedEditorOptions = void 0; - this.editorControl.dispose(); super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index e58de5a1c6e..dfa60893d52 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -25,6 +25,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWindowService } from 'vs/platform/windows/common/windows'; /** * An editor implementation that is capable of showing the contents of resource inputs. Uses @@ -41,12 +42,13 @@ export class AbstractTextResourceEditor extends BaseTextEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IWindowService windowService: IWindowService ) { - super(id, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService); + super(id, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService); } - public getTitle(): string { + getTitle(): string { if (this.input) { return this.input.getName(); } @@ -54,14 +56,14 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return nls.localize('textEditor', "Text Editor"); } - public setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Thenable { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then((resolvedModel: EditorModel) => { + return input.resolve().then((resolvedModel: EditorModel) => { // Check for cancellation if (token.isCancellationRequested) { @@ -104,7 +106,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } } - public setOptions(options: EditorOptions): void { + setOptions(options: EditorOptions): void { const textOptions = options; if (textOptions && types.isFunction(textOptions.apply)) { textOptions.apply(this.getControl(), ScrollType.Smooth); @@ -140,7 +142,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { * This allows users to click on the output panel to stop scrolling when they see something of interest. * To resume, they should scroll to the end of the output panel again. */ - public revealLastLine(smart: boolean): void { + revealLastLine(smart: boolean): void { const codeEditor = this.getControl(); const model = codeEditor.getModel(); @@ -152,7 +154,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } } - public clearInput(): void { + clearInput(): void { // Keep editor view state in settings to restore when coming back this.saveTextResourceEditorViewState(this.input); @@ -163,7 +165,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { super.clearInput(); } - public shutdown(): void { + shutdown(): void { // Save View State (only for untitled) if (this.input instanceof UntitledEditorInput) { @@ -200,7 +202,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { export class TextResourceEditor extends AbstractTextResourceEditor { - public static readonly ID = 'workbench.editors.textResourceEditor'; + static readonly ID = 'workbench.editors.textResourceEditor'; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -210,8 +212,9 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IWindowService windowService: IWindowService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorGroupService, textFileService, editorService); + super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorGroupService, textFileService, editorService, windowService); } } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8ddd3329287..297bac18a96 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -5,38 +5,41 @@ 'use strict'; -import 'vs/css!./media/titlecontrol'; -import { localize } from 'vs/nls'; -import { prepareActions } from 'vs/workbench/browser/actions'; -import { IAction, Action, IRunEvent } from 'vs/base/common/actions'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import * as arrays from 'vs/base/common/arrays'; -import { toResource, IEditorCommandsContext, IEditorInput, EditorCommandsContextActionRunner } from 'vs/workbench/common/editor'; -import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { createActionItem, fillInContextMenuActions, fillInActionBarActions } from 'vs/platform/actions/browser/menuItemActionItem'; -import { IMenuService, MenuId, IMenu, ExecuteCommandAction } from 'vs/platform/actions/common/actions'; -import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { Themable } from 'vs/workbench/common/theme'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IEditorGroupsAccessor, IEditorPartOptions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { LocalSelectionTransfer, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; import { applyDragImage } from 'vs/base/browser/dnd'; +import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { ActionsOrientation, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Action, IAction, IRunEvent } from 'vs/base/common/actions'; +import * as arrays from 'vs/base/common/arrays'; +import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import 'vs/css!./media/titlecontrol'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { createActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { prepareActions } from 'vs/workbench/browser/actions'; +import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; +import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; +import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource } from 'vs/workbench/common/editor'; +import { ResourceContextKey } from 'vs/workbench/common/resources'; +import { Themable } from 'vs/workbench/common/theme'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export interface IToolbarActions { primary: IAction[]; @@ -48,6 +51,8 @@ export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); + protected breadcrumbsControl: BreadcrumbsControl; + private currentPrimaryEditorActionIds: string[] = []; private currentSecondaryEditorActionIds: string[] = []; protected editorActionsToolbar: ToolBar; @@ -72,7 +77,8 @@ export abstract class TitleControl extends Themable { @IMenuService private menuService: IMenuService, @IQuickOpenService protected quickOpenService: IQuickOpenService, @IThemeService themeService: IThemeService, - @IExtensionService private extensionService: IExtensionService + @IExtensionService private extensionService: IExtensionService, + @IConfigurationService protected configurationService: IConfigurationService ) { super(themeService); @@ -89,6 +95,27 @@ export abstract class TitleControl extends Themable { protected abstract create(parent: HTMLElement): void; + protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void { + const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService)); + this._register(config.onDidChange(() => { + const value = config.getValue(); + if (!value && this.breadcrumbsControl) { + this.breadcrumbsControl.dispose(); + this.breadcrumbsControl = undefined; + this.handleBreadcrumbsEnablementChange(); + } else if (value && !this.breadcrumbsControl) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + this.breadcrumbsControl.update(); + this.handleBreadcrumbsEnablementChange(); + } + })); + if (config.getValue()) { + this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); + } + } + + protected abstract handleBreadcrumbsEnablementChange(): void; + protected createEditorActionsToolBar(container: HTMLElement): void { const context = { groupId: this.group.id } as IEditorCommandsContext; @@ -327,9 +354,18 @@ export abstract class TitleControl extends Themable { layout(dimension: Dimension): void { // Optionally implemented in subclasses + + if (this.breadcrumbsControl) { + this.breadcrumbsControl.layout(undefined); + } + } + + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden() ? BreadcrumbsControl.HEIGHT : 0); } dispose(): void { + this.breadcrumbsControl = dispose(this.breadcrumbsControl); this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables); super.dispose(); diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index 73fad28fcc0..24552a04bd6 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -115,7 +115,6 @@ /** Notification: Source */ .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-source { - opacity: 0.7; flex: 1; font-size: 12px; overflow: hidden; /* always give away space to buttons container */ diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 71923117b10..da0751e329b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -18,8 +18,8 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService export class ClearNotificationAction extends Action { - public static readonly ID = CLEAR_NOTIFICATION; - public static readonly LABEL = localize('clearNotification', "Clear Notification"); + static readonly ID = CLEAR_NOTIFICATION; + static readonly LABEL = localize('clearNotification', "Clear Notification"); constructor( id: string, @@ -29,7 +29,7 @@ export class ClearNotificationAction extends Action { super(id, label, 'clear-notification-action'); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.commandService.executeCommand(CLEAR_NOTIFICATION, notification); return TPromise.as(void 0); @@ -38,8 +38,8 @@ export class ClearNotificationAction extends Action { export class ClearAllNotificationsAction extends Action { - public static readonly ID = CLEAR_ALL_NOTIFICATIONS; - public static readonly LABEL = localize('clearNotifications', "Clear All Notifications"); + static readonly ID = CLEAR_ALL_NOTIFICATIONS; + static readonly LABEL = localize('clearNotifications', "Clear All Notifications"); constructor( id: string, @@ -49,7 +49,7 @@ export class ClearAllNotificationsAction extends Action { super(id, label, 'clear-all-notifications-action'); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS); return TPromise.as(void 0); @@ -58,8 +58,8 @@ export class ClearAllNotificationsAction extends Action { export class HideNotificationsCenterAction extends Action { - public static readonly ID = HIDE_NOTIFICATIONS_CENTER; - public static readonly LABEL = localize('hideNotificationsCenter', "Hide Notifications"); + static readonly ID = HIDE_NOTIFICATIONS_CENTER; + static readonly LABEL = localize('hideNotificationsCenter', "Hide Notifications"); constructor( id: string, @@ -69,7 +69,7 @@ export class HideNotificationsCenterAction extends Action { super(id, label, 'hide-all-notifications-action'); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER); return TPromise.as(void 0); @@ -78,8 +78,8 @@ export class HideNotificationsCenterAction extends Action { export class ExpandNotificationAction extends Action { - public static readonly ID = EXPAND_NOTIFICATION; - public static readonly LABEL = localize('expandNotification', "Expand Notification"); + static readonly ID = EXPAND_NOTIFICATION; + static readonly LABEL = localize('expandNotification', "Expand Notification"); constructor( id: string, @@ -89,7 +89,7 @@ export class ExpandNotificationAction extends Action { super(id, label, 'expand-notification-action'); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.commandService.executeCommand(EXPAND_NOTIFICATION, notification); return TPromise.as(void 0); @@ -98,8 +98,8 @@ export class ExpandNotificationAction extends Action { export class CollapseNotificationAction extends Action { - public static readonly ID = COLLAPSE_NOTIFICATION; - public static readonly LABEL = localize('collapseNotification', "Collapse Notification"); + static readonly ID = COLLAPSE_NOTIFICATION; + static readonly LABEL = localize('collapseNotification', "Collapse Notification"); constructor( id: string, @@ -109,7 +109,7 @@ export class CollapseNotificationAction extends Action { super(id, label, 'collapse-notification-action'); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification); return TPromise.as(void 0); @@ -118,8 +118,8 @@ export class CollapseNotificationAction extends Action { export class ConfigureNotificationAction extends Action { - public static readonly ID = 'workbench.action.configureNotification'; - public static readonly LABEL = localize('configureNotification', "Configure Notification"); + static readonly ID = 'workbench.action.configureNotification'; + static readonly LABEL = localize('configureNotification', "Configure Notification"); constructor( id: string, @@ -129,15 +129,15 @@ export class ConfigureNotificationAction extends Action { super(id, label, 'configure-notification-action'); } - public get configurationActions(): IAction[] { + get configurationActions(): IAction[] { return this._configurationActions; } } export class CopyNotificationMessageAction extends Action { - public static readonly ID = 'workbench.action.copyNotificationMessage'; - public static readonly LABEL = localize('copyNotification', "Copy Text"); + static readonly ID = 'workbench.action.copyNotificationMessage'; + static readonly LABEL = localize('copyNotification', "Copy Text"); constructor( id: string, @@ -147,7 +147,7 @@ export class CopyNotificationMessageAction extends Action { super(id, label); } - public run(notification: INotificationViewItem): TPromise { + run(notification: INotificationViewItem): TPromise { this.clipboardService.writeText(notification.message.raw); return TPromise.as(void 0); @@ -174,7 +174,7 @@ export class NotificationActionRunner extends ActionRunner { this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'message' }); // Run and make sure to notify on any error again - super.runAction(action, context).done(null, error => this.notificationService.error(error)); + super.runAction(action, context).then(null, error => this.notificationService.error(error)); return TPromise.as(void 0); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index 9d7e25af5ab..f6c6d6379f3 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts @@ -8,15 +8,14 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { localize } from 'vs/nls'; import { INotificationViewItem, INotificationsModel, NotificationChangeType, INotificationChangeEvent } from 'vs/workbench/common/notifications'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Severity } from 'vs/platform/notification/common/notification'; -export class NotificationsAlerts { - private toDispose: IDisposable[]; +export class NotificationsAlerts extends Disposable { constructor(private model: INotificationsModel) { - this.toDispose = []; + super(); // Alert initial notifications if any model.notifications.forEach(n => this.ariaAlert(n)); @@ -25,7 +24,7 @@ export class NotificationsAlerts { } private registerListeners(): void { - this.toDispose.push(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); } private onDidNotificationChange(e: INotificationChangeEvent): void { @@ -57,8 +56,4 @@ export class NotificationsAlerts { alert(alertText); } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 750aeb9764f..c1aae8802de 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -29,13 +29,15 @@ export class NotificationsCenter extends Themable { private static MAX_DIMENSIONS = new Dimension(450, 400); + private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); + get onDidChangeVisibility(): Event { return this._onDidChangeVisibility.event; } + private notificationsCenterContainer: HTMLElement; private notificationsCenterHeader: HTMLElement; private notificationsCenterTitle: HTMLSpanElement; private notificationsList: NotificationsList; private _isVisible: boolean; private workbenchDimensions: Dimension; - private readonly _onDidChangeVisibility: Emitter; private notificationsCenterVisibleContextKey: IContextKey; constructor( @@ -50,27 +52,20 @@ export class NotificationsCenter extends Themable { ) { super(themeService); - this._onDidChangeVisibility = new Emitter(); - this.toUnbind.push(this._onDidChangeVisibility); - this.notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(contextKeyService); this.registerListeners(); } private registerListeners(): void { - this.toUnbind.push(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); } - public get onDidChangeVisibility(): Event { - return this._onDidChangeVisibility.event; - } - - public get isVisible(): boolean { + get isVisible(): boolean { return this._isVisible; } - public show(): void { + show(): void { if (this._isVisible) { this.notificationsList.show(true /* focus */); @@ -111,9 +106,9 @@ export class NotificationsCenter extends Themable { private updateTitle(): void { if (this.model.notifications.length === 0) { - this.notificationsCenterTitle.innerText = localize('notificationsEmpty', "No new notifications"); + this.notificationsCenterTitle.textContent = localize('notificationsEmpty', "No new notifications"); } else { - this.notificationsCenterTitle.innerText = localize('notifications', "Notifications"); + this.notificationsCenterTitle.textContent = localize('notifications', "Notifications"); } } @@ -138,21 +133,17 @@ export class NotificationsCenter extends Themable { addClass(toolbarContainer, 'notifications-center-header-toolbar'); this.notificationsCenterHeader.appendChild(toolbarContainer); - const actionRunner = this.instantiationService.createInstance(NotificationActionRunner); - this.toUnbind.push(actionRunner); + const actionRunner = this._register(this.instantiationService.createInstance(NotificationActionRunner)); - const notificationsToolBar = new ActionBar(toolbarContainer, { + const notificationsToolBar = this._register(new ActionBar(toolbarContainer, { ariaLabel: localize('notificationsToolbar', "Notification Center Actions"), actionRunner - }); - this.toUnbind.push(notificationsToolBar); + })); - const hideAllAction = this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL); - this.toUnbind.push(hideAllAction); + const hideAllAction = this._register(this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL)); notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) }); - const clearAllAction = this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL); - this.toUnbind.push(clearAllAction); + const clearAllAction = this._register(this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL)); notificationsToolBar.push(clearAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(clearAllAction) }); // Notifications List @@ -204,7 +195,7 @@ export class NotificationsCenter extends Themable { } } - public hide(): void { + hide(): void { if (!this._isVisible || !this.notificationsCenterContainer) { return; // already hidden } @@ -244,7 +235,7 @@ export class NotificationsCenter extends Themable { } } - public layout(dimension: Dimension): void { + layout(dimension: Dimension): void { this.workbenchDimensions = dimension; if (this._isVisible && this.notificationsCenterContainer) { @@ -278,7 +269,7 @@ export class NotificationsCenter extends Themable { } } - public clearAll(): void { + clearAll(): void { // Hide notifications center first this.hide(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 858f3bbe848..9519578fe60 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -7,7 +7,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { INotificationViewItem, isNotificationViewItem } from 'vs/workbench/common/notifications'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; @@ -88,7 +88,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Hide Notifications Center KeybindingsRegistry.registerCommandAndKeybindingRule({ id: HIDE_NOTIFICATIONS_CENTER, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, when: NotificationsCenterVisibleContext, primary: KeyCode.Escape, handler: accessor => center.hide() @@ -106,7 +106,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Clear Notification KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CLEAR_NOTIFICATION, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.Delete, mac: { @@ -123,7 +123,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Expand Notification KeybindingsRegistry.registerCommandAndKeybindingRule({ id: EXPAND_NOTIFICATION, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.RightArrow, handler: (accessor, args?: any) => { @@ -137,7 +137,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Collapse Notification KeybindingsRegistry.registerCommandAndKeybindingRule({ id: COLLAPSE_NOTIFICATION, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.LeftArrow, handler: (accessor, args?: any) => { @@ -151,7 +151,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Toggle Notification KeybindingsRegistry.registerCommandAndKeybindingRule({ id: TOGGLE_NOTIFICATION, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.Space, secondary: [KeyCode.Enter], @@ -166,7 +166,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Hide Toasts KeybindingsRegistry.registerCommandAndKeybindingRule({ id: HIDE_NOTIFICATION_TOAST, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, when: NotificationsToastsVisibleContext, primary: KeyCode.Escape, handler: accessor => toasts.hide() @@ -178,7 +178,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Focus Next Toast KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FOCUS_NEXT_NOTIFICATION_TOAST, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext), primary: KeyCode.DownArrow, handler: (accessor) => { @@ -189,7 +189,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Focus Previous Toast KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FOCUS_PREVIOUS_NOTIFICATION_TOAST, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext), primary: KeyCode.UpArrow, handler: (accessor) => { @@ -200,7 +200,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Focus First Toast KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FOCUS_FIRST_NOTIFICATION_TOAST, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext), primary: KeyCode.PageUp, secondary: [KeyCode.Home], @@ -212,7 +212,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Focus Last Toast KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FOCUS_LAST_NOTIFICATION_TOAST, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(NotificationFocusedContext, NotificationsToastsVisibleContext), primary: KeyCode.PageDown, secondary: [KeyCode.End], diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 3f4cb3ad968..094a62f91fd 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -38,7 +38,7 @@ export class NotificationsList extends Themable { this.viewModel = []; } - public show(focus?: boolean): void { + show(focus?: boolean): void { if (this.isVisible) { if (focus) { this.list.domFocus(); @@ -67,47 +67,43 @@ export class NotificationsList extends Themable { this.listContainer = document.createElement('div'); addClass(this.listContainer, 'notifications-list-container'); - const actionRunner = this.instantiationService.createInstance(NotificationActionRunner); - this.toUnbind.push(actionRunner); + const actionRunner = this._register(this.instantiationService.createInstance(NotificationActionRunner)); // Notification Renderer const renderer = this.instantiationService.createInstance(NotificationRenderer, actionRunner); // List - this.list = >this.instantiationService.createInstance( + this.list = this._register(>this.instantiationService.createInstance( WorkbenchList, this.listContainer, new NotificationsListDelegate(this.listContainer), [renderer], this.options - ); - this.toUnbind.push(this.list); + )); // Context menu to copy message - const copyAction = this.instantiationService.createInstance(CopyNotificationMessageAction, CopyNotificationMessageAction.ID, CopyNotificationMessageAction.LABEL); - this.toUnbind.push(copyAction); - this.toUnbind.push(this.list.onContextMenu(e => { + const copyAction = this._register(this.instantiationService.createInstance(CopyNotificationMessageAction, CopyNotificationMessageAction.ID, CopyNotificationMessageAction.LABEL)); + this._register((this.list.onContextMenu(e => { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => TPromise.as([copyAction]), getActionsContext: () => e.element, actionRunner }); - })); + }))); // Toggle on double click - this.toUnbind.push(this.list.onMouseDblClick(event => (event.element as INotificationViewItem).toggle())); + this._register((this.list.onMouseDblClick(event => (event.element as INotificationViewItem).toggle()))); // Clear focus when DOM focus moves out // Use document.hasFocus() to not clear the focus when the entire window lost focus // This ensures that when the focus comes back, the notifciation is still focused - const listFocusTracker = trackFocus(this.list.getHTMLElement()); - listFocusTracker.onDidBlur(() => { + const listFocusTracker = this._register(trackFocus(this.list.getHTMLElement())); + this._register(listFocusTracker.onDidBlur(() => { if (document.hasFocus()) { this.list.setFocus([]); } - }); - this.toUnbind.push(listFocusTracker); + })); // Context key NotificationFocusedContext.bindTo(this.list.contextKeyService); @@ -115,7 +111,7 @@ export class NotificationsList extends Themable { // Only allow for focus in notifications, as the // selection is too strong over the contents of // the notification - this.toUnbind.push(this.list.onSelectionChange(e => { + this._register(this.list.onSelectionChange(e => { if (e.indexes.length > 0) { this.list.setSelection([]); } @@ -126,7 +122,7 @@ export class NotificationsList extends Themable { this.updateStyles(); } - public updateNotificationsList(start: number, deleteCount: number, items: INotificationViewItem[] = []) { + updateNotificationsList(start: number, deleteCount: number, items: INotificationViewItem[] = []) { const listHasDOMFocus = isAncestor(document.activeElement, this.listContainer); // Remember focus and relative top of that item @@ -177,7 +173,7 @@ export class NotificationsList extends Themable { } } - public hide(): void { + hide(): void { if (!this.isVisible || !this.list) { return; // already hidden } @@ -192,7 +188,7 @@ export class NotificationsList extends Themable { this.viewModel = []; } - public focusFirst(): void { + focusFirst(): void { if (!this.isVisible || !this.list) { return; // hidden } @@ -201,7 +197,7 @@ export class NotificationsList extends Themable { this.list.domFocus(); } - public hasFocus(): boolean { + hasFocus(): boolean { if (!this.isVisible || !this.list) { return false; // hidden } @@ -222,7 +218,7 @@ export class NotificationsList extends Themable { } } - public layout(width: number, maxHeight?: number): void { + layout(width: number, maxHeight?: number): void { if (this.list) { this.listContainer.style.width = `${width}px`; @@ -234,7 +230,7 @@ export class NotificationsList extends Themable { } } - public dispose(): void { + dispose(): void { this.hide(); super.dispose(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 32f884d0fc6..fe7e047fc1b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -7,13 +7,12 @@ import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem } from 'vs/workbench/common/notifications'; import { IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { localize } from 'vs/nls'; -export class NotificationsStatus { +export class NotificationsStatus extends Disposable { private statusItem: IDisposable; - private toDispose: IDisposable[]; private isNotificationsCenterVisible: boolean; private _counter: Set; @@ -21,7 +20,8 @@ export class NotificationsStatus { private model: INotificationsModel, @IStatusbarService private statusbarService: IStatusbarService ) { - this.toDispose = []; + super(); + this._counter = new Set(); this.updateNotificationsStatusItem(); @@ -33,7 +33,7 @@ export class NotificationsStatus { return this._counter.size; } - public update(isCenterVisible: boolean): void { + update(isCenterVisible: boolean): void { if (this.isNotificationsCenterVisible !== isCenterVisible) { this.isNotificationsCenterVisible = isCenterVisible; @@ -44,7 +44,7 @@ export class NotificationsStatus { } private registerListeners(): void { - this.toDispose.push(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); } private onDidNotificationChange(e: INotificationChangeEvent): void { @@ -101,8 +101,8 @@ export class NotificationsStatus { return localize('notifications', "{0} New Notifications", this.count); } - public dispose() { - this.toDispose = dispose(this.toDispose); + dispose() { + super.dispose(); if (this.statusItem) { this.statusItem.dispose(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 3c51fb8c2f0..17eb959b455 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -80,7 +80,7 @@ export class NotificationsToasts extends Themable { } private registerListeners(): void { - this.toUnbind.push(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); } private onDidNotificationChange(e: INotificationChangeEvent): void { @@ -185,7 +185,7 @@ export class NotificationsToasts extends Themable { let timeoutHandle: number; const hideAfterTimeout = () => { timeoutHandle = setTimeout(() => { - const showsProgress = item.progress && !item.progress.state.done; + const showsProgress = item.hasProgress() && !item.progress.state.done; if (!notificationList.hasFocus() && !item.expanded && !isMouseOverToast && !showsProgress) { this.removeToast(item); } else { @@ -265,7 +265,7 @@ export class NotificationsToasts extends Themable { this.notificationsToastsVisibleContextKey.set(false); } - public hide(): void { + hide(): void { const focusGroup = isAncestor(document.activeElement, this.notificationsToastsContainer); this.removeToasts(); @@ -275,7 +275,7 @@ export class NotificationsToasts extends Themable { } } - public focus(): boolean { + focus(): boolean { const toasts = this.getToasts(ToastVisibility.VISIBLE); if (toasts.length > 0) { toasts[0].list.focusFirst(); @@ -286,7 +286,7 @@ export class NotificationsToasts extends Themable { return false; } - public focusNext(): boolean { + focusNext(): boolean { const toasts = this.getToasts(ToastVisibility.VISIBLE); for (let i = 0; i < toasts.length; i++) { const toast = toasts[i]; @@ -305,7 +305,7 @@ export class NotificationsToasts extends Themable { return false; } - public focusPrevious(): boolean { + focusPrevious(): boolean { const toasts = this.getToasts(ToastVisibility.VISIBLE); for (let i = 0; i < toasts.length; i++) { const toast = toasts[i]; @@ -324,7 +324,7 @@ export class NotificationsToasts extends Themable { return false; } - public focusFirst(): boolean { + focusFirst(): boolean { const toast = this.getToasts(ToastVisibility.VISIBLE)[0]; if (toast) { toast.list.focusFirst(); @@ -335,7 +335,7 @@ export class NotificationsToasts extends Themable { return false; } - public focusLast(): boolean { + focusLast(): boolean { const toasts = this.getToasts(ToastVisibility.VISIBLE); if (toasts.length > 0) { toasts[toasts.length - 1].list.focusFirst(); @@ -346,7 +346,7 @@ export class NotificationsToasts extends Themable { return false; } - public update(isCenterVisible: boolean): void { + update(isCenterVisible: boolean): void { if (this.isNotificationsCenterVisible !== isCenterVisible) { this.isNotificationsCenterVisible = isCenterVisible; @@ -391,7 +391,7 @@ export class NotificationsToasts extends Themable { return notificationToasts.reverse(); // from newest to oldest } - public layout(dimension: Dimension): void { + layout(dimension: Dimension): void { this.workbenchDimensions = dimension; const maxDimensions = this.computeMaxDimensions(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 035b52716f8..055f280bb41 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -5,10 +5,10 @@ 'use strict'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; -import { clearNode, addClass, removeClass, toggleClass, addDisposableListener } from 'vs/base/browser/dom'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { localize } from 'vs/nls'; import { ButtonGroup } from 'vs/base/browser/ui/button/button'; @@ -26,7 +26,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Severity } from 'vs/platform/notification/common/notification'; -export class NotificationsListDelegate implements IDelegate { +export class NotificationsListDelegate implements IVirtualDelegate { private static readonly ROW_HEIGHT = 42; private static readonly LINE_HEIGHT = 22; @@ -46,7 +46,7 @@ export class NotificationsListDelegate implements IDelegate { - public static readonly TEMPLATE_ID = 'notification'; + static readonly TEMPLATE_ID = 'notification'; constructor( private actionRunner: IActionRunner, @@ -191,11 +191,11 @@ export class NotificationRenderer implements IRenderer { + if (e.button === 1 /* Middle Button */) { + EventHelper.stop(e); + + notification.close(); + } + })); // Severity Icon this.renderSeverity(notification); @@ -417,10 +426,10 @@ export class NotificationTemplateRenderer { private renderSource(notification): void { if (notification.expanded && notification.source) { - this.template.source.innerText = localize('notificationSource', "Source: {0}", notification.source); + this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source); this.template.source.title = notification.source; } else { - this.template.source.innerText = ''; + this.template.source.textContent = ''; this.template.source.removeAttribute('title'); } } @@ -435,8 +444,7 @@ export class NotificationTemplateRenderer { button.label = action.label; this.inputDisposeables.push(button.onDidClick(e => { - e.preventDefault(); - e.stopPropagation(); + EventHelper.stop(e, true); // Run action this.actionRunner.run(action, notification); @@ -501,7 +509,7 @@ export class NotificationTemplateRenderer { return keybinding ? keybinding.getLabel() : void 0; } - public dispose(): void { + dispose(): void { this.inputDisposeables = dispose(this.inputDisposeables); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 0c58440f0d4..cfd1137cf35 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -81,16 +81,19 @@ } .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge { - margin-left: 4px; + margin-left: 8px; } .monaco-workbench > .part.panel > .title > .panel-switcher-container > .monaco-action-bar .badge .badge-content { - padding: 0.2em 0.5em; + padding: 0.3em 0.5em; border-radius: 1em; font-weight: normal; text-align: center; - display: inline; -} + display: inline-block; + min-width: 1.6em; + line-height: 1em; + box-sizing: border-box; + } /** Actions */ diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index b1451168b72..54d4f61d040 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -10,14 +10,15 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/actions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService'; -import { ActivityAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; +import { ActivityAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { IActivity } from 'vs/workbench/common/activity'; export class ClosePanelAction extends Action { + static readonly ID = 'workbench.action.closePanel'; static LABEL = nls.localize('closePanel', "Close Panel"); @@ -29,12 +30,13 @@ export class ClosePanelAction extends Action { super(id, name, 'hide-panel-action'); } - public run(): TPromise { + run(): TPromise { return this.partService.setPanelHidden(true); } } export class TogglePanelAction extends Action { + static readonly ID = 'workbench.action.togglePanel'; static LABEL = nls.localize('togglePanel', "Toggle Panel"); @@ -46,15 +48,15 @@ export class TogglePanelAction extends Action { super(id, name, partService.isVisible(Parts.PANEL_PART) ? 'panel expanded' : 'panel'); } - public run(): TPromise { + run(): TPromise { return this.partService.setPanelHidden(this.partService.isVisible(Parts.PANEL_PART)); } } class FocusPanelAction extends Action { - public static readonly ID = 'workbench.action.focusPanel'; - public static readonly LABEL = nls.localize('focusPanel', "Focus into Panel"); + static readonly ID = 'workbench.action.focusPanel'; + static readonly LABEL = nls.localize('focusPanel', "Focus into Panel"); constructor( id: string, @@ -65,7 +67,7 @@ class FocusPanelAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { // Show panel if (!this.partService.isVisible(Parts.PANEL_PART)) { @@ -83,10 +85,12 @@ class FocusPanelAction extends Action { export class TogglePanelPositionAction extends Action { - public static readonly ID = 'workbench.action.togglePanelPosition'; - public static readonly LABEL = nls.localize('toggledPanelPosition', "Toggle Panel Position"); + static readonly ID = 'workbench.action.togglePanelPosition'; + static readonly LABEL = nls.localize('toggledPanelPosition', "Toggle Panel Position"); + private static readonly MOVE_TO_RIGHT_LABEL = nls.localize('moveToRight', "Move to Right"); private static readonly MOVE_TO_BOTTOM_LABEL = nls.localize('moveToBottom', "Move to Bottom"); + private toDispose: IDisposable[]; constructor( @@ -106,12 +110,12 @@ export class TogglePanelPositionAction extends Action { setClassAndLabel(); } - public run(): TPromise { + run(): TPromise { const position = this.partService.getPanelPosition(); return this.partService.setPanelPosition(position === Position.BOTTOM ? Position.RIGHT : Position.BOTTOM); } - public dispose(): void { + dispose(): void { super.dispose(); this.toDispose = dispose(this.toDispose); } @@ -119,10 +123,12 @@ export class TogglePanelPositionAction extends Action { export class ToggleMaximizedPanelAction extends Action { - public static readonly ID = 'workbench.action.toggleMaximizedPanel'; - public static readonly LABEL = nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"); + static readonly ID = 'workbench.action.toggleMaximizedPanel'; + static readonly LABEL = nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"); + private static readonly MAXIMIZE_LABEL = nls.localize('maximizePanel', "Maximize Panel Size"); private static readonly RESTORE_LABEL = nls.localize('minimizePanel', "Restore Panel Size"); + private toDispose: IDisposable[]; constructor( @@ -139,13 +145,12 @@ export class ToggleMaximizedPanelAction extends Action { })); } - public run(): TPromise { - // Show panel + run(): TPromise { return (!this.partService.isVisible(Parts.PANEL_PART) ? this.partService.setPanelHidden(false) : TPromise.as(null)) .then(() => this.partService.toggleMaximizedPanel()); } - public dispose(): void { + dispose(): void { super.dispose(); this.toDispose = dispose(this.toDispose); } @@ -160,7 +165,7 @@ export class PanelActivityAction extends ActivityAction { super(activity); } - public run(event: any): TPromise { + run(event: any): TPromise { return this.panelService.openPanel(this.activity.id, true).then(() => this.activate()); } } @@ -172,3 +177,12 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedP actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL), 'View: Close Panel', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL), 'View: Toggle Panel Position', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, undefined), 'View: Toggle Panel Position', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '2_workbench_layout', + command: { + id: TogglePanelAction.ID, + title: nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel") + }, + order: 5 +}); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index f076f3a9ff1..1880db49212 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -7,7 +7,6 @@ import 'vs/css!./media/panelpart'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; -import { $ } from 'vs/base/browser/builder'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPanel } from 'vs/workbench/common/panel'; @@ -24,13 +23,13 @@ import { ClosePanelAction, TogglePanelPositionAction, PanelActivityAction, Toggl import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; -import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar'; -import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; +import { CompositeBar } from 'vs/workbench/browser/parts/compositeBar'; +import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const ActivePanleContextId = 'activePanel'; @@ -38,18 +37,18 @@ export const ActivePanelContext = new RawContextKey(ActivePanleContextId export class PanelPart extends CompositePart implements IPanelService { - public static readonly activePanelSettingsKey = 'workbench.panelpart.activepanelid'; + static readonly activePanelSettingsKey = 'workbench.panelpart.activepanelid'; + private static readonly PINNED_PANELS = 'workbench.panel.pinnedPanels'; private static readonly MIN_COMPOSITE_BAR_WIDTH = 50; - public _serviceBrand: any; + _serviceBrand: any; private activePanelContextKey: IContextKey; private blockOpeningPanel: boolean; private compositeBar: CompositeBar; - private compositeActions: { [compositeId: string]: { activityAction: PanelActivityAction, pinnedAction: ToggleCompositePinnedAction } }; + private compositeActions: { [compositeId: string]: { activityAction: PanelActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null); private dimension: Dimension; - private disposables: IDisposable[] = []; constructor( id: string, @@ -82,8 +81,7 @@ export class PanelPart extends CompositePart implements IPanelService { { hasTitle: true } ); - this.compositeActions = Object.create(null); - this.compositeBar = this.instantiationService.createInstance(CompositeBar, { + this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, { icon: false, storageId: PanelPart.PINNED_PANELS, orientation: ActionsOrientation.HORIZONTAL, @@ -102,31 +100,31 @@ export class PanelPart extends CompositePart implements IPanelService { badgeForeground, dragAndDropBackground: PANEL_DRAG_AND_DROP_BACKGROUND } - }); - this.toUnbind.push(this.compositeBar); + })); + for (const panel of this.getPanels()) { this.compositeBar.addComposite(panel); } + this.activePanelContextKey = ActivePanelContext.bindTo(contextKeyService); - this.onDidPanelOpen(this._onDidPanelOpen, this, this.disposables); - this.onDidPanelClose(this._onDidPanelClose, this, this.disposables); this.registerListeners(); } private registerListeners(): void { + this._register(this.onDidPanelOpen(this._onDidPanelOpen, this)); + this._register(this.onDidPanelClose(this._onDidPanelClose, this)); - this.toUnbind.push(this.registry.onDidRegister(panelDescriptor => this.compositeBar.addComposite(panelDescriptor))); + this._register(this.registry.onDidRegister(panelDescriptor => this.compositeBar.addComposite(panelDescriptor))); // Activate panel action on opening of a panel - this.toUnbind.push(this.onDidPanelOpen(panel => { + this._register(this.onDidPanelOpen(panel => { this.compositeBar.activateComposite(panel.getId()); - // Need to relayout composite bar since different panels have different action bar width - this.layoutCompositeBar(); + this.layoutCompositeBar(); // Need to relayout composite bar since different panels have different action bar width })); // Deactivate panel action on close - this.toUnbind.push(this.onDidPanelClose(panel => this.compositeBar.deactivateComposite(panel.getId()))); + this._register(this.onDidPanelClose(panel => this.compositeBar.deactivateComposite(panel.getId()))); } private _onDidPanelOpen(panel: IPanel): void { @@ -141,26 +139,26 @@ export class PanelPart extends CompositePart implements IPanelService { } } - public get onDidPanelOpen(): Event { + get onDidPanelOpen(): Event { return this._onDidCompositeOpen.event; } - public get onDidPanelClose(): Event { + get onDidPanelClose(): Event { return this._onDidCompositeClose.event; } - public updateStyles(): void { + updateStyles(): void { super.updateStyles(); - const container = $(this.getContainer()); - container.style('background-color', this.getColor(PANEL_BACKGROUND)); - container.style('border-left-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder)); + const container = this.getContainer(); + container.style.backgroundColor = this.getColor(PANEL_BACKGROUND); + container.style.borderLeftColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder); - const title = $(this.getTitleArea()); - title.style('border-top-color', this.getColor(PANEL_BORDER) || this.getColor(contrastBorder)); + const title = this.getTitleArea(); + title.style.borderTopColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder); } - public openPanel(id: string, focus?: boolean): TPromise { + openPanel(id: string, focus?: boolean): TPromise { if (this.blockOpeningPanel) { return TPromise.as(null); // Workaround against a potential race condition } @@ -179,7 +177,7 @@ export class PanelPart extends CompositePart implements IPanelService { return promise.then(() => this.openComposite(id, focus)); } - public showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable { + showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable { return this.compositeBar.showActivity(panelId, badge, clazz); } @@ -187,13 +185,13 @@ export class PanelPart extends CompositePart implements IPanelService { return this.getPanels().filter(p => p.id === panelId).pop(); } - public getPanels(): PanelDescriptor[] { + getPanels(): PanelDescriptor[] { return Registry.as(PanelExtensions.Panels).getPanels() .filter(p => p.enabled) .sort((v1, v2) => v1.order - v2.order); } - public setPanelEnablement(id: string, enabled: boolean): void { + setPanelEnablement(id: string, enabled: boolean): void { const descriptor = Registry.as(PanelExtensions.Panels).getPanels().filter(p => p.id === id).pop(); if (descriptor && descriptor.enabled !== enabled) { descriptor.enabled = enabled; @@ -213,15 +211,15 @@ export class PanelPart extends CompositePart implements IPanelService { ]; } - public getActivePanel(): IPanel { + getActivePanel(): IPanel { return this.getActiveComposite(); } - public getLastActivePanelId(): string { + getLastActivePanelId(): string { return this.getLastActiveCompositetId(); } - public hideActivePanel(): TPromise { + hideActivePanel(): TPromise { return this.hideActiveComposite().then(composite => void 0); } @@ -242,7 +240,7 @@ export class PanelPart extends CompositePart implements IPanelService { }; } - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { if (!this.partService.isVisible(Parts.PANEL_PART)) { return [dimension]; } @@ -283,7 +281,7 @@ export class PanelPart extends CompositePart implements IPanelService { } private removeComposite(compositeId: string): void { - this.compositeBar.removeComposite(compositeId); + this.compositeBar.hideComposite(compositeId); const compositeActions = this.compositeActions[compositeId]; if (compositeActions) { compositeActions.activityAction.dispose(); @@ -299,11 +297,6 @@ export class PanelPart extends CompositePart implements IPanelService { } return this.toolBar.getItemsWidth(); } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } } registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/browser/parts/quickinput/media/dark/arrow-left.svg b/src/vs/workbench/browser/parts/quickinput/media/dark/arrow-left.svg new file mode 100644 index 00000000000..0d24c218ae1 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/media/dark/arrow-left.svg @@ -0,0 +1,12 @@ + + + + arrow-left + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/quickinput/media/light/arrow-left.svg b/src/vs/workbench/browser/parts/quickinput/media/light/arrow-left.svg new file mode 100644 index 00000000000..b8362b27458 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/media/light/arrow-left.svg @@ -0,0 +1,12 @@ + + + + arrow-left + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts index 5aea4a4cfce..7e5bc7f43a0 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.contribution.ts @@ -4,7 +4,15 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { QuickPickManyToggle } from 'vs/workbench/browser/parts/quickinput/quickInput'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { QuickPickManyToggle, BackAction } from 'vs/workbench/browser/parts/quickinput/quickInput'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen'; KeybindingsRegistry.registerCommandAndKeybindingRule(QuickPickManyToggle); + +const registry = Registry.as(ActionExtensions.WorkbenchActions); +registry.registerWorkbenchAction(new SyncActionDescriptor(BackAction, BackAction.ID, BackAction.LABEL, { primary: null, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.US_MINUS }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_MINUS } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Back'); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.css b/src/vs/workbench/browser/parts/quickinput/quickInput.css index 630d3d6ba5f..113c80a9abc 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.css +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.css @@ -12,6 +12,39 @@ margin-left: -300px; } +.quick-input-titlebar { + display: flex; +} + +.quick-input-left-action-bar { + display: flex; + margin-left: 4px; + flex: 1; +} + +.quick-input-left-action-bar.monaco-action-bar .actions-container { + justify-content: flex-start; +} + +.quick-input-title { + padding: 3px 0px; + text-align: center; +} + +.quick-input-right-action-bar { + display: flex; + margin-right: 4px; + flex: 1; +} + +.quick-input-titlebar .monaco-action-bar .action-label.icon { + margin: 0; + width: 19px; + height: 100%; + background-position: center; + background-repeat: no-repeat; +} + .quick-input-header { display: flex; padding: 6px 6px 4px 6px; @@ -32,10 +65,15 @@ flex-grow: 1; } -.quick-input-widget[data-type=pickMany] .quick-input-box { +.quick-input-widget.show-checkboxes .quick-input-box { margin-left: 5px; } +.quick-input-visible-count { + position: absolute; + left: -10000px; +} + .quick-input-count { align-self: center; position: absolute; @@ -60,6 +98,10 @@ margin: 0px 11px; } +.quick-input-progress.monaco-progress-container { + position: relative; +} + .quick-input-progress.monaco-progress-container, .quick-input-progress.monaco-progress-container .progress-bit { height: 2px; @@ -75,12 +117,22 @@ } .quick-input-list .quick-input-list-entry { + box-sizing: border-box; overflow: hidden; display: flex; height: 100%; padding: 0 6px; } +.quick-input-list .quick-input-list-entry.quick-input-list-separator-border { + border-top-width: 1px; + border-top-style: solid; +} + +.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border { + border-top-style: none; +} + .quick-input-list .quick-input-list-label { overflow: hidden; display: flex; @@ -103,10 +155,17 @@ margin-left: 5px; } -.quick-input-widget[data-type=pickMany] .quick-input-list .quick-input-list-rows { +.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows { margin-left: 10px; } +.quick-input-widget .quick-input-list .quick-input-list-checkbox { + display: none; +} +.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox { + display: inline; +} + .quick-input-list .quick-input-list-rows > .quick-input-list-row { display: flex; align-items: center; @@ -119,8 +178,50 @@ .quick-input-list .quick-input-list-label-meta { opacity: 0.7; line-height: normal; + text-overflow: ellipsis; + overflow: hidden; } .quick-input-list .monaco-highlighted-label .highlight { font-weight: bold; -} \ No newline at end of file +} + +.quick-input-list .quick-input-list-separator { + margin-right: 18px; +} + +.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-separator, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry.has-actions .quick-input-list-separator { + margin-right: 0; +} + +.quick-input-list .quick-input-list-entry-action-bar { + display: none; + flex: 0; + overflow: visible; +} + +.quick-input-list .quick-input-list-entry-action-bar .action-label.icon { + margin: 0; + width: 19px; + height: 100%; + background-position: center; + background-repeat: no-repeat; +} + +.quick-input-list .quick-input-list-entry-action-bar { + margin-top: 1px; +} + +.quick-input-list .quick-input-list-entry-action-bar ul:first-child .action-label.icon { + margin-left: 2px; +} + +.quick-input-list .quick-input-list-entry-action-bar ul:last-child .action-label.icon { + margin-right: 8px; +} + +.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar, +.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar { + display: flex; +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index dffd902c812..9c83f648d77 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -7,7 +7,7 @@ import 'vs/css!./quickInput'; import { Component } from 'vs/workbench/common/component'; -import { IQuickInputService, IPickOpenEntry, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import * as dom from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -16,7 +16,7 @@ import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegi import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { TPromise } from 'vs/base/common/winjs.base'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList } from './quickInputList'; import { QuickInputBox } from './quickInputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -28,136 +28,514 @@ import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { attachBadgeStyler, attachProgressBarStyler, attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { chain, debounceEvent } from 'vs/base/common/event'; +import { debounceEvent, Emitter, Event } from 'vs/base/common/event'; import { Button } from 'vs/base/browser/ui/button/button'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { onUnexpectedError, canceled } from 'vs/base/common/errors'; import Severity from 'vs/base/common/severity'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ICommandAndKeybindingRule, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICommandAndKeybindingRule, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen'; +import { ActionBar, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { URI } from 'vs/base/common/uri'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { getIconClass } from 'vs/workbench/browser/parts/quickinput/quickInputUtils'; +import { AccessibilitySupport } from 'vs/base/common/platform'; +import * as browser from 'vs/base/browser/browser'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; const $ = dom.$; -type InputParameters = PickOneParameters | PickManyParameters | TextInputParameters; +type Writeable = { -readonly [P in keyof T]: T[P] }; -export interface BaseInputParameters { - readonly type: 'pickOne' | 'pickMany' | 'textInput'; - readonly ignoreFocusLost?: boolean; -} - -export interface PickParameters extends BaseInputParameters { - readonly type: 'pickOne' | 'pickMany'; - readonly picks: TPromise; - readonly matchOnDescription?: boolean; - readonly matchOnDetail?: boolean; - readonly placeHolder?: string; -} - -export interface PickOneParameters extends PickParameters { - readonly type: 'pickOne'; -} - -export interface PickManyParameters extends PickParameters { - readonly type: 'pickMany'; -} - -export interface TextInputParameters extends BaseInputParameters { - readonly type: 'textInput'; - readonly value?: string; - readonly valueSelection?: [number, number]; - readonly prompt?: string; - readonly placeHolder?: string; - readonly password?: boolean; - readonly validateInput?: (input: string) => TPromise; -} +const backButton = { + iconPath: { + dark: URI.parse(require.toUrl('vs/workbench/browser/parts/quickinput/media/dark/arrow-left.svg')), + light: URI.parse(require.toUrl('vs/workbench/browser/parts/quickinput/media/light/arrow-left.svg')) + }, + tooltip: localize('quickInput.back', "Back"), + handle: -1 // TODO +}; interface QuickInputUI { container: HTMLElement; + leftActionBar: ActionBar; + title: HTMLElement; + rightActionBar: ActionBar; checkAll: HTMLInputElement; inputBox: QuickInputBox; + visibleCount: CountBadge; count: CountBadge; message: HTMLElement; + progressBar: ProgressBar; list: QuickInputList; - close: (ok?: true | Thenable) => void; + onDidAccept: Event; + onDidTriggerButton: Event; + ignoreFocusOut: boolean; + keyMods: Writeable; + isScreenReaderOptimized(): boolean; + show(controller: QuickInput): void; + setVisibilities(visibilities: Visibilities): void; + setComboboxAccessibility(enabled: boolean): void; + setEnabled(enabled: boolean): void; + setContextKey(contextKey?: string): void; + hide(): void; } -interface InputController { - readonly showUI: { [k in keyof QuickInputUI]?: boolean; } & { ok?: boolean; }; - readonly result: TPromise; - readonly ready: TPromise; - readonly resolve: (ok?: true | Thenable) => void | TPromise; +type Visibilities = { + title?: boolean; + checkAll?: boolean; + inputBox?: boolean; + visibleCount?: boolean; + count?: boolean; + message?: boolean; + list?: boolean; + ok?: boolean; +}; + +class QuickInput implements IQuickInput { + + private _title: string; + private _steps: number; + private _totalSteps: number; + protected visible = false; + private _enabled = true; + private _contextKey: string; + private _busy = false; + private _ignoreFocusOut = false; + private _buttons: IQuickInputButton[] = []; + private buttonsUpdated = false; + private onDidTriggerButtonEmitter = new Emitter(); + private onDidHideEmitter = new Emitter(); + + protected visibleDisposables: IDisposable[] = []; + protected disposables: IDisposable[] = [ + this.onDidTriggerButtonEmitter, + this.onDidHideEmitter, + ]; + + private busyDelay: TimeoutTimer; + + constructor(protected ui: QuickInputUI) { + } + + get title() { + return this._title; + } + + set title(title: string) { + this._title = title; + this.update(); + } + + get step() { + return this._steps; + } + + set step(step: number) { + this._steps = step; + this.update(); + } + + get totalSteps() { + return this._totalSteps; + } + + set totalSteps(totalSteps: number) { + this._totalSteps = totalSteps; + this.update(); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled: boolean) { + this._enabled = enabled; + this.update(); + } + + get contextKey() { + return this._contextKey; + } + + set contextKey(contextKey: string) { + this._contextKey = contextKey; + this.update(); + } + + get busy() { + return this._busy; + } + + set busy(busy: boolean) { + this._busy = busy; + this.update(); + } + + get ignoreFocusOut() { + return this._ignoreFocusOut; + } + + set ignoreFocusOut(ignoreFocusOut: boolean) { + this._ignoreFocusOut = ignoreFocusOut; + this.update(); + } + + get buttons() { + return this._buttons; + } + + set buttons(buttons: IQuickInputButton[]) { + this._buttons = buttons; + this.buttonsUpdated = true; + this.update(); + } + + onDidTriggerButton = this.onDidTriggerButtonEmitter.event; + + show(): void { + if (this.visible) { + return; + } + this.visibleDisposables.push( + this.ui.onDidTriggerButton(button => { + if (this.buttons.indexOf(button) !== -1) { + this.onDidTriggerButtonEmitter.fire(button); + } + }), + ); + this.ui.show(this); + this.visible = true; + this.update(); + } + + hide(): void { + if (!this.visible) { + return; + } + this.ui.hide(); + } + + didHide(): void { + this.visible = false; + this.visibleDisposables = dispose(this.visibleDisposables); + this.onDidHideEmitter.fire(); + } + + onDidHide = this.onDidHideEmitter.event; + + protected update() { + if (!this.visible) { + return; + } + const title = this.getTitle(); + if (this.ui.title.textContent !== title) { + this.ui.title.textContent = title; + } + if (this.busy && !this.busyDelay) { + this.busyDelay = new TimeoutTimer(); + this.busyDelay.setIfNotSet(() => { + if (this.visible) { + this.ui.progressBar.infinite(); + } + }, 800); + } + if (!this.busy && this.busyDelay) { + this.ui.progressBar.stop(); + this.busyDelay.cancel(); + this.busyDelay = null; + } + if (this.buttonsUpdated) { + this.buttonsUpdated = false; + this.ui.leftActionBar.clear(); + const leftButtons = this.buttons.filter(button => button === backButton); + this.ui.leftActionBar.push(leftButtons.map((button, index) => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button)); + action.tooltip = button.tooltip; + return action; + }), { icon: true, label: false }); + this.ui.rightActionBar.clear(); + const rightButtons = this.buttons.filter(button => button !== backButton); + this.ui.rightActionBar.push(rightButtons.map((button, index) => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => this.onDidTriggerButtonEmitter.fire(button)); + action.tooltip = button.tooltip; + return action; + }), { icon: true, label: false }); + } + this.ui.ignoreFocusOut = this.ignoreFocusOut; + this.ui.setEnabled(this.enabled); + this.ui.setContextKey(this.contextKey); + } + + private getTitle() { + if (this.title && this.step) { + return `${this.title} (${this.getSteps()})`; + } + if (this.title) { + return this.title; + } + if (this.step) { + return this.getSteps(); + } + return ''; + } + + private getSteps() { + if (this.step && this.totalSteps) { + return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps); + } + if (this.step) { + return String(this.step); + } + return ''; + } + + public dispose(): void { + this.hide(); + this.disposables = dispose(this.disposables); + } } -class PickOneController implements InputController { - public showUI = { inputBox: true, list: true }; - public result: TPromise; - public ready: TPromise; - public resolve: (ok?: true | Thenable) => void; - public progress: (value: T) => void; - private closed = false; - private quickNavigate = false; - private disposables: IDisposable[] = []; +class QuickPick extends QuickInput implements IQuickPick { - constructor(private ui: QuickInputUI, parameters: PickOneParameters) { - this.result = new TPromise((resolve, reject, progress) => { - this.resolve = ok => resolve(ok === true ? ui.list.getFocusedElements()[0] : ok); - this.progress = progress; - }); - this.result.then(() => this.dispose()); + private _value = ''; + private _placeholder; + private onDidChangeValueEmitter = new Emitter(); + private onDidAcceptEmitter = new Emitter(); + private _items: (T | IQuickPickSeparator)[] = []; + private itemsUpdated = false; + private _canSelectMany = false; + private _matchOnDescription = false; + private _matchOnDetail = false; + private _activeItems: T[] = []; + private activeItemsUpdated = false; + private activeItemsToConfirm: T[] = []; + private onDidChangeActiveEmitter = new Emitter(); + private _selectedItems: T[] = []; + private selectedItemsUpdated = false; + private selectedItemsToConfirm: T[] = []; + private onDidChangeSelectionEmitter = new Emitter(); + private onDidTriggerItemButtonEmitter = new Emitter>(); - ui.inputBox.value = ''; - ui.inputBox.setPlaceholder(parameters.placeHolder || ''); - ui.list.matchOnDescription = parameters.matchOnDescription; - ui.list.matchOnDetail = parameters.matchOnDetail; - ui.list.setElements([]); + quickNavigate: IQuickNavigateConfiguration; - this.ready = parameters.picks.then(elements => { - if (this.closed) { + constructor(ui: QuickInputUI) { + super(ui); + this.disposables.push( + this.onDidChangeValueEmitter, + this.onDidAcceptEmitter, + this.onDidChangeActiveEmitter, + this.onDidChangeSelectionEmitter, + this.onDidTriggerItemButtonEmitter, + ); + } + + get value() { + return this._value; + } + + set value(value: string) { + this._value = value || ''; + this.update(); + } + + get placeholder() { + return this._placeholder; + } + + set placeholder(placeholder: string) { + this._placeholder = placeholder; + this.update(); + } + + onDidChangeValue = this.onDidChangeValueEmitter.event; + + onDidAccept = this.onDidAcceptEmitter.event; + + get items() { + return this._items; + } + + set items(items: (T | IQuickPickSeparator)[]) { + this._items = items; + this.itemsUpdated = true; + this.update(); + } + + get canSelectMany() { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + this.update(); + } + + get matchOnDescription() { + return this._matchOnDescription; + } + + set matchOnDescription(matchOnDescription: boolean) { + this._matchOnDescription = matchOnDescription; + this.update(); + } + + get matchOnDetail() { + return this._matchOnDetail; + } + + set matchOnDetail(matchOnDetail: boolean) { + this._matchOnDetail = matchOnDetail; + this.update(); + } + + get activeItems() { + return this._activeItems; + } + + set activeItems(activeItems: T[]) { + this._activeItems = activeItems; + this.activeItemsUpdated = true; + this.update(); + } + + onDidChangeActive = this.onDidChangeActiveEmitter.event; + + get selectedItems() { + return this._selectedItems; + } + + set selectedItems(selectedItems: T[]) { + this._selectedItems = selectedItems; + this.selectedItemsUpdated = true; + this.update(); + } + + get keyMods() { + return this.ui.keyMods; + } + + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; + + onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; + + show() { + if (!this.visible) { + this.visibleDisposables.push( + this.ui.inputBox.onDidChange(value => { + if (value === this.value) { + return; + } + this._value = value; + this.ui.list.filter(this.ui.inputBox.value); + if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) { + this.ui.list.focus('First'); + } + this.onDidChangeValueEmitter.fire(value); + }), + this.ui.inputBox.onKeyDown(event => { + switch (event.keyCode) { + case KeyCode.DownArrow: + this.ui.list.focus('Next'); + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + break; + case KeyCode.UpArrow: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('Previous'); + } else { + this.ui.list.focus('Last'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + break; + case KeyCode.PageDown: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('NextPage'); + } else { + this.ui.list.focus('First'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + break; + case KeyCode.PageUp: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('PreviousPage'); + } else { + this.ui.list.focus('Last'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + break; + } + }), + this.ui.onDidAccept(() => { + if (!this.canSelectMany && this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + } + this.onDidAcceptEmitter.fire(); + }), + this.ui.list.onDidChangeFocus(focusedItems => { + if (this.activeItemsUpdated) { + return; // Expect another event. + } + if (this.activeItemsToConfirm !== this._activeItems && equals(focusedItems, this._activeItems, (a, b) => a === b)) { + return; + } + this._activeItems = focusedItems as T[]; + this.onDidChangeActiveEmitter.fire(focusedItems as T[]); + }), + this.ui.list.onDidChangeSelection(selectedItems => { + if (this.canSelectMany) { + if (selectedItems.length) { + this.ui.list.setSelectedElements([]); + } + return; + } + if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) { + return; + } + this._selectedItems = selectedItems as T[]; + this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); + this.onDidAcceptEmitter.fire(); + }), + this.ui.list.onChangedCheckedElements(checkedItems => { + if (!this.canSelectMany) { + return; + } + if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) { + return; + } + this._selectedItems = checkedItems as T[]; + this.onDidChangeSelectionEmitter.fire(checkedItems as T[]); + }), + this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event as IQuickPickItemButtonEvent)), + this.registerQuickNavigation() + ); + } + super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.) + } + + private registerQuickNavigation() { + return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + if (this.canSelectMany || !this.quickNavigate) { return; } - ui.list.setElements(elements); - ui.list.filter(ui.inputBox.value); - ui.list.focus('First'); - - this.disposables.push( - ui.list.onSelectionChange(elements => { - if (elements[0]) { - ui.close(true); - } - }), - ui.inputBox.onDidChange(value => { - ui.list.filter(value); - ui.list.focus('First'); - }), - ui.inputBox.onKeyDown(event => { - switch (event.keyCode) { - case KeyCode.DownArrow: - ui.list.focus('Next'); - break; - case KeyCode.UpArrow: - ui.list.focus('Previous'); - break; - } - }) - ); - }); - } - - configureQuickNavigate(quickNavigate: IQuickNavigateConfiguration) { - if (this.quickNavigate) { - return; - } - this.quickNavigate = true; - - this.disposables.push(dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, (e: KeyboardEvent) => { const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e as KeyboardEvent); const keyCode = keyboardEvent.keyCode; // Select element when keys are pressed that signal it - const quickNavKeys = quickNavigate.keybindings; + const quickNavKeys = this.quickNavigate.keybindings; const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some(k => { const [firstPart, chordPart] = k.getParts(); if (chordPart) { @@ -187,159 +565,195 @@ class PickOneController implements InputController return false; }); - if (wasTriggerKeyPressed) { - this.ui.close(true); + if (wasTriggerKeyPressed && this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + this.onDidAcceptEmitter.fire(); } - })); + }); } - private dispose() { - this.closed = true; - this.disposables = dispose(this.disposables); + protected update() { + super.update(); + if (!this.visible) { + return; + } + if (this.ui.inputBox.value !== this.value) { + this.ui.inputBox.value = this.value; + } + if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { + this.ui.inputBox.placeholder = (this.placeholder || ''); + } + if (this.itemsUpdated) { + this.itemsUpdated = false; + this.ui.list.setElements(this.items); + this.ui.list.filter(this.ui.inputBox.value); + this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); + this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); + this.ui.count.setCount(this.ui.list.getCheckedCount()); + if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) { + this.ui.list.focus('First'); + } + } + if (this.ui.container.classList.contains('show-checkboxes') !== this.canSelectMany) { + if (this.canSelectMany) { + this.ui.list.clearFocus(); + } else if (!this.ui.isScreenReaderOptimized()) { + this.ui.list.focus('First'); + } + } + if (this.activeItemsUpdated) { + this.activeItemsUpdated = false; + this.activeItemsToConfirm = this._activeItems; + this.ui.list.setFocusedElements(this.activeItems); + if (this.activeItemsToConfirm === this._activeItems) { + this.activeItemsToConfirm = null; + } + } + if (this.selectedItemsUpdated) { + this.selectedItemsUpdated = false; + this.selectedItemsToConfirm = this._selectedItems; + if (this.canSelectMany) { + this.ui.list.setCheckedElements(this.selectedItems); + } else { + this.ui.list.setSelectedElements(this.selectedItems); + } + if (this.selectedItemsToConfirm === this._selectedItems) { + this.selectedItemsToConfirm = null; + } + } + this.ui.list.matchOnDescription = this.matchOnDescription; + this.ui.list.matchOnDetail = this.matchOnDetail; + this.ui.setComboboxAccessibility(true); + this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: true, list: true } : { title: !!this.title || !!this.step, inputBox: true, visibleCount: true, list: true }); } } -class PickManyController implements InputController { - public showUI = { checkAll: true, inputBox: true, count: true, ok: true, list: true }; - public result: TPromise; - public ready: TPromise; - public resolve: (ok?: true | Thenable) => void; - public progress: (value: T) => void; - private closed = false; - private disposables: IDisposable[] = []; +class InputBox extends QuickInput implements IInputBox { - constructor(ui: QuickInputUI, parameters: PickManyParameters) { - this.result = new TPromise((resolve, reject, progress) => { - this.resolve = ok => resolve(ok === true ? ui.list.getCheckedElements() : ok); - this.progress = progress; - }); - this.result.then(() => this.dispose()); + private static noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); - ui.inputBox.value = ''; - ui.inputBox.setPlaceholder(parameters.placeHolder || ''); - ui.list.matchOnDescription = parameters.matchOnDescription; - ui.list.matchOnDetail = parameters.matchOnDetail; - ui.list.setElements([]); - ui.checkAll.checked = ui.list.getAllVisibleChecked(); - ui.count.setCount(ui.list.getCheckedCount()); + private _value = ''; + private _valueSelection: Readonly<[number, number]>; + private valueSelectionUpdated = true; + private _placeholder: string; + private _password = false; + private _prompt: string; + private noValidationMessage = InputBox.noPromptMessage; + private _validationMessage: string; + private onDidValueChangeEmitter = new Emitter(); + private onDidAcceptEmitter = new Emitter(); - this.ready = parameters.picks.then(elements => { - if (this.closed) { - return; - } + constructor(ui: QuickInputUI) { + super(ui); + this.disposables.push( + this.onDidValueChangeEmitter, + this.onDidAcceptEmitter, + ); + } - ui.list.setElements(elements, true); - ui.list.filter(ui.inputBox.value); - ui.checkAll.checked = ui.list.getAllVisibleChecked(); - ui.count.setCount(ui.list.getCheckedCount()); + get value() { + return this._value; + } - this.disposables.push( - ui.inputBox.onDidChange(value => { - ui.list.filter(value); + set value(value: string) { + this._value = value || ''; + this.update(); + } + + set valueSelection(valueSelection: Readonly<[number, number]>) { + this._valueSelection = valueSelection; + this.valueSelectionUpdated = true; + this.update(); + } + + get placeholder() { + return this._placeholder; + } + + set placeholder(placeholder: string) { + this._placeholder = placeholder; + this.update(); + } + + get password() { + return this._password; + } + + set password(password: boolean) { + this._password = password; + this.update(); + } + + get prompt() { + return this._prompt; + } + + set prompt(prompt: string) { + this._prompt = prompt; + this.noValidationMessage = prompt + ? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt) + : InputBox.noPromptMessage; + this.update(); + } + + get validationMessage() { + return this._validationMessage; + } + + set validationMessage(validationMessage: string) { + this._validationMessage = validationMessage; + this.update(); + } + + onDidChangeValue = this.onDidValueChangeEmitter.event; + + onDidAccept = this.onDidAcceptEmitter.event; + + show() { + if (!this.visible) { + this.visibleDisposables.push( + this.ui.inputBox.onDidChange(value => { + if (value === this.value) { + return; + } + this._value = value; + this.onDidValueChangeEmitter.fire(value); }), - ui.inputBox.onKeyDown(event => { - switch (event.keyCode) { - case KeyCode.DownArrow: - ui.list.focus('First'); - ui.list.domFocus(); - break; - case KeyCode.UpArrow: - ui.list.focus('Last'); - ui.list.domFocus(); - break; - } - }) + this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()), ); - }); - } - - private dispose() { - this.closed = true; - this.disposables = dispose(this.disposables); - } -} - -class TextInputController implements InputController { - public showUI = { inputBox: true, message: true }; - public result: TPromise; - public ready = TPromise.as(null); - public resolveResult: (string) => void; - private validationValue: string; - private validation: TPromise; - private defaultMessage: string; - private disposables: IDisposable[] = []; - - constructor(private ui: QuickInputUI, private parameters: TextInputParameters) { - this.result = new TPromise((resolve, reject, progress) => { - this.resolveResult = resolve; - }); - this.result.then(() => this.dispose()); - - ui.inputBox.value = parameters.value || ''; - const selection = parameters.valueSelection; - ui.inputBox.select(selection && { start: selection[0], end: selection[1] }); - ui.inputBox.setPlaceholder(parameters.placeHolder || ''); - this.defaultMessage = parameters.prompt - ? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", parameters.prompt) - : localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); - ui.message.textContent = this.defaultMessage; - ui.inputBox.setPassword(parameters.password); - - if (parameters.validateInput) { - const onDidChange = debounceEvent(ui.inputBox.onDidChange, (last, cur) => cur, 100); - this.disposables.push(onDidChange(() => this.didChange())); - if (ui.inputBox.value) { - // Replicating old behavior: only fire if value is not empty. - this.didChange(); - } + this.valueSelectionUpdated = true; } + super.show(); } - didChange() { - this.updatedValidation() - .then(validationError => { - this.ui.message.textContent = validationError || this.defaultMessage; - this.ui.inputBox.showDecoration(validationError ? Severity.Error : Severity.Ignore); - }) - .then(null, onUnexpectedError); - } - - resolve(ok?: true | Thenable) { - if (ok === true) { - return this.updatedValidation() - .then(validationError => { - if (validationError) { - throw canceled(); - } - this.resolveResult(this.ui.inputBox.value); - }); - } else { - this.resolveResult(ok); + protected update() { + super.update(); + if (!this.visible) { + return; } - return null; - } - - private updatedValidation() { - if (this.parameters.validateInput) { - const value = this.ui.inputBox.value; - if (value !== this.validationValue) { - this.validationValue = value; - this.validation = this.parameters.validateInput(value) - .then(validationError => { - if (this.validationValue !== value) { - throw canceled(); - } - return validationError; - }); - } - } else if (!this.validation) { - this.validation = TPromise.as(null); + if (this.ui.inputBox.value !== this.value) { + this.ui.inputBox.value = this.value; } - return this.validation; - } - - private dispose() { - this.disposables = dispose(this.disposables); + if (this.valueSelectionUpdated) { + this.valueSelectionUpdated = false; + this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] }); + } + if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { + this.ui.inputBox.placeholder = (this.placeholder || ''); + } + if (this.ui.inputBox.password !== this.password) { + this.ui.inputBox.password = this.password; + } + if (!this.validationMessage && this.ui.message.textContent !== this.noValidationMessage) { + this.ui.message.textContent = this.noValidationMessage; + this.ui.inputBox.showDecoration(Severity.Ignore); + } + if (this.validationMessage && this.ui.message.textContent !== this.validationMessage) { + this.ui.message.textContent = this.validationMessage; + this.ui.inputBox.showDecoration(Severity.Error); + } + this.ui.setVisibilities({ title: !!this.title || !!this.step, inputBox: true, message: true }); } } @@ -350,19 +764,24 @@ export class QuickInputService extends Component implements IQuickInputService { private static readonly ID = 'workbench.component.quickinput'; private static readonly MAX_WIDTH = 600; // Max total width of quick open widget + private idPrefix = 'quickInput_'; // Constant since there is still only one. private layoutDimensions: dom.Dimension; + private titleBar: HTMLElement; private filterContainer: HTMLElement; + private visibleCountContainer: HTMLElement; private countContainer: HTMLElement; private okContainer: HTMLElement; + private ok: Button; private ui: QuickInputUI; - private ready = false; - private progressBar: ProgressBar; - private ignoreFocusLost = false; + private comboboxAccessibility = false; + private enabled = true; private inQuickOpenWidgets: Record = {}; private inQuickOpenContext: IContextKey; + private contexts: { [id: string]: IContextKey; } = Object.create(null); + private onDidAcceptEmitter = this._register(new Emitter()); + private onDidTriggerButtonEmitter = this._register(new Emitter()); - private controller: InputController; - private multiStepHandle: CancellationTokenSource; + private controller: QuickInput; constructor( @IEnvironmentService private environmentService: IEnvironmentService, @@ -371,13 +790,14 @@ export class QuickInputService extends Component implements IQuickInputService { @IPartService private partService: IPartService, @IQuickOpenService private quickOpenService: IQuickOpenService, @IEditorGroupsService private editorGroupService: IEditorGroupsService, - @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService private keybindingService: IKeybindingService, + @IContextKeyService private contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService ) { super(QuickInputService.ID, themeService); this.inQuickOpenContext = new RawContextKey('inQuickOpen', false).bindTo(contextKeyService); - this.toUnbind.push(this.quickOpenService.onShow(() => this.inQuickOpen('quickOpen', true))); - this.toUnbind.push(this.quickOpenService.onHide(() => this.inQuickOpen('quickOpen', false))); + this._register(this.quickOpenService.onShow(() => this.inQuickOpen('quickOpen', true))); + this._register(this.quickOpenService.onHide(() => this.inQuickOpen('quickOpen', false))); } private inQuickOpen(widget: 'quickInput' | 'quickOpen', open: boolean) { @@ -397,25 +817,65 @@ export class QuickInputService extends Component implements IQuickInputService { } } + private setContextKey(id?: string) { + let key: IContextKey; + if (id) { + key = this.contexts[id]; + if (!key) { + key = new RawContextKey(id, false) + .bindTo(this.contextKeyService); + this.contexts[id] = key; + } + } + + if (key && key.get()) { + return; // already active context + } + + this.resetContextKeys(); + + if (key) { + key.set(true); + } + } + + private resetContextKeys() { + for (const key in this.contexts) { + if (this.contexts[key].get()) { + this.contexts[key].reset(); + } + } + } + private create() { if (this.ui) { return; } - const workbench = document.getElementById(this.partService.getWorkbenchElementId()); - const container = dom.append(workbench, $('.quick-input-widget')); + const workbench = this.partService.getWorkbenchElement(); + const container = dom.append(workbench, $('.quick-input-widget.show-file-icons')); container.tabIndex = -1; container.style.display = 'none'; + this.titleBar = dom.append(container, $('.quick-input-titlebar')); + + const leftActionBar = this._register(new ActionBar(this.titleBar)); + leftActionBar.domNode.classList.add('quick-input-left-action-bar'); + + const title = dom.append(this.titleBar, $('.quick-input-title')); + + const rightActionBar = this._register(new ActionBar(this.titleBar)); + rightActionBar.domNode.classList.add('quick-input-right-action-bar'); + const headerContainer = dom.append(container, $('.quick-input-header')); const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); checkAll.type = 'checkbox'; - this.toUnbind.push(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { + this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { const checked = checkAll.checked; list.setAllVisibleChecked(checked); })); - this.toUnbind.push(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { + this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space'... inputBox.setFocus(); } @@ -423,252 +883,422 @@ export class QuickInputService extends Component implements IQuickInputService { this.filterContainer = dom.append(headerContainer, $('.quick-input-filter')); - const inputBox = new QuickInputBox(this.filterContainer); - this.toUnbind.push(inputBox); + const inputBox = this._register(new QuickInputBox(this.filterContainer)); + + this.visibleCountContainer = dom.append(this.filterContainer, $('.quick-input-visible-count')); + this.visibleCountContainer.setAttribute('aria-live', 'polite'); + this.visibleCountContainer.setAttribute('aria-atomic', 'true'); + const visibleCount = new CountBadge(this.visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") }); this.countContainer = dom.append(this.filterContainer, $('.quick-input-count')); + this.countContainer.setAttribute('aria-live', 'polite'); const count = new CountBadge(this.countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }); - this.toUnbind.push(attachBadgeStyler(count, this.themeService)); + this._register(attachBadgeStyler(count, this.themeService)); this.okContainer = dom.append(headerContainer, $('.quick-input-action')); - const ok = new Button(this.okContainer); - attachButtonStyler(ok, this.themeService); - ok.label = localize('ok', "OK"); - this.toUnbind.push(ok.onDidClick(e => { - if (this.ready) { - this.close(true); - } + this.ok = new Button(this.okContainer); + attachButtonStyler(this.ok, this.themeService); + this.ok.label = localize('ok', "OK"); + this._register(this.ok.onDidClick(e => { + this.onDidAcceptEmitter.fire(); })); const message = dom.append(container, $('.quick-input-message')); - this.progressBar = new ProgressBar(container); - dom.addClass(this.progressBar.getContainer(), 'quick-input-progress'); - this.toUnbind.push(attachProgressBarStyler(this.progressBar, this.themeService)); + const progressBar = new ProgressBar(container); + dom.addClass(progressBar.getContainer(), 'quick-input-progress'); + this._register(attachProgressBarStyler(progressBar, this.themeService)); - const list = this.instantiationService.createInstance(QuickInputList, container); - this.toUnbind.push(list); - this.toUnbind.push(list.onAllVisibleCheckedChanged(checked => { + const list = this._register(this.instantiationService.createInstance(QuickInputList, container, this.idPrefix + 'list')); + this._register(list.onChangedAllVisibleChecked(checked => { checkAll.checked = checked; })); - this.toUnbind.push(list.onCheckedCountChanged(c => { + this._register(list.onChangedVisibleCount(c => { + visibleCount.setCount(c); + })); + this._register(list.onChangedCheckedCount(c => { count.setCount(c); })); - this.toUnbind.push(list.onLeave(() => { + this._register(list.onLeave(() => { // Defer to avoid the input field reacting to the triggering key. setTimeout(() => { inputBox.setFocus(); - list.clearFocus(); + if (this.controller instanceof QuickPick && this.controller.canSelectMany) { + list.clearFocus(); + } }, 0); })); - this.toUnbind.push( - chain(list.onFocusChange) - .map(e => e[0]) - .filter(e => !!e) - .latch() - .on(e => { - // TODO - if (this.controller instanceof PickOneController || this.controller instanceof PickManyController) { - this.controller.progress(e); - } - }) - ); - - this.toUnbind.push(dom.addDisposableListener(container, 'focusout', (e: FocusEvent) => { - if (e.relatedTarget === container) { - (e.target).focus(); - return; - } - for (let element = e.relatedTarget; element; element = element.parentElement) { - if (element === container) { - return; - } - } - if (!this.ignoreFocusLost && !this.environmentService.args['sticky-quickopen'] && this.configurationService.getValue(CLOSE_ON_FOCUS_LOST_CONFIG)) { - this.close(undefined, true); + this._register(list.onDidChangeFocus(() => { + if (this.comboboxAccessibility) { + this.ui.inputBox.setAttribute('aria-activedescendant', this.ui.list.getActiveDescendant()); } })); - this.toUnbind.push(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + + const focusTracker = dom.trackFocus(container); + this._register(focusTracker); + this._register(focusTracker.onDidBlur(() => { + if (!this.ui.ignoreFocusOut && !this.environmentService.args['sticky-quickopen'] && this.configurationService.getValue(CLOSE_ON_FOCUS_LOST_CONFIG)) { + this.hide(true); + } + })); + this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); switch (event.keyCode) { case KeyCode.Enter: - if (this.ready) { - dom.EventHelper.stop(e, true); - this.close(true); - } + dom.EventHelper.stop(e, true); + this.onDidAcceptEmitter.fire(); break; case KeyCode.Escape: dom.EventHelper.stop(e, true); - this.close(); + this.hide(); break; case KeyCode.Tab: if (!event.altKey && !event.ctrlKey && !event.metaKey) { - const inputs = [].slice.call(container.querySelectorAll('input')) - .filter(input => input.style.display !== 'none'); - if (event.shiftKey && event.target === inputs[0]) { + const selectors = ['.action-label.icon']; + if (container.classList.contains('show-checkboxes')) { + selectors.push('input'); + } else { + selectors.push('input[type=text]'); + } + if (this.ui.list.isDisplayed()) { + selectors.push('.monaco-list'); + } + const stops = container.querySelectorAll(selectors.join(', ')); + if (event.shiftKey && event.target === stops[0]) { dom.EventHelper.stop(e, true); - inputs[inputs.length - 1].focus(); - } else if (!event.shiftKey && event.target === inputs[inputs.length - 1]) { + stops[stops.length - 1].focus(); + } else if (!event.shiftKey && event.target === stops[stops.length - 1]) { dom.EventHelper.stop(e, true); - inputs[0].focus(); + stops[0].focus(); } } break; } })); + this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Ctrl: + case KeyCode.Meta: + this.ui.keyMods.ctrlCmd = true; + break; + case KeyCode.Alt: + this.ui.keyMods.alt = true; + break; + } + })); + this._register(dom.addDisposableListener(container, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Ctrl: + case KeyCode.Meta: + this.ui.keyMods.ctrlCmd = false; + break; + case KeyCode.Alt: + this.ui.keyMods.alt = false; + break; + } + })); - this.toUnbind.push(this.quickOpenService.onShow(() => this.close())); + this._register(this.quickOpenService.onShow(() => this.hide(true))); - this.ui = { container, checkAll, inputBox, count, message, list, close: ok => this.close(ok) }; + this.ui = { + container, + leftActionBar, + title, + rightActionBar, + checkAll, + inputBox, + visibleCount, + count, + message, + progressBar, + list, + onDidAccept: this.onDidAcceptEmitter.event, + onDidTriggerButton: this.onDidTriggerButtonEmitter.event, + ignoreFocusOut: false, + keyMods: { ctrlCmd: false, alt: false }, + isScreenReaderOptimized: () => this.isScreenReaderOptimized(), + show: controller => this.show(controller), + hide: () => this.hide(), + setVisibilities: visibilities => this.setVisibilities(visibilities), + setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled), + setEnabled: enabled => this.setEnabled(enabled), + setContextKey: contextKey => this.setContextKey(contextKey), + }; this.updateStyles(); } - private close(ok?: true | Thenable, focusLost?: boolean) { - if (!this.isDisplayed()) { - return TPromise.as(undefined); - } - if (this.controller) { - const resolved = this.controller.resolve(ok); - if (resolved) { - const result = resolved - .then(() => { - this.inQuickOpen('quickInput', false); - this.ui.container.style.display = 'none'; - if (!focusLost) { - this.editorGroupService.activeGroup.focus(); + pick>(picks: TPromise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): TPromise { + return new TPromise((doResolve, reject) => { + let resolve = (result: any) => { + resolve = doResolve; + if (options.onKeyMods) { + options.onKeyMods(input.keyMods); + } + doResolve(result); + }; + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createQuickPick(); + let activeItem: T; + const disposables = [ + input, + input.onDidAccept(() => { + if (input.canSelectMany) { + resolve(input.selectedItems.slice()); + input.hide(); + } else { + const result = input.activeItems[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidChangeActive(items => { + const focused = items[0]; + if (focused && options.onDidFocus) { + options.onDidFocus(focused); + } + }), + input.onDidChangeSelection(items => { + if (!input.canSelectMany) { + const result = items[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({ + ...event, + removeItem: () => { + const index = input.items.indexOf(event.item); + if (index !== -1) { + const items = input.items.slice(); + items.splice(index, 1); + input.items = items; + } + } + })), + input.onDidChangeValue(value => { + if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) { + input.activeItems = [activeItem]; + } + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + input.canSelectMany = options.canPickMany; + input.placeholder = options.placeHolder; + input.ignoreFocusOut = options.ignoreFocusLost; + input.matchOnDescription = options.matchOnDescription; + input.matchOnDetail = options.matchOnDetail; + input.quickNavigate = options.quickNavigate; + input.contextKey = options.contextKey; + input.busy = true; + TPromise.join([picks, options.activeItem]) + .then(([items, _activeItem]) => { + activeItem = _activeItem; + input.busy = false; + input.items = items; + if (input.canSelectMany) { + input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[]; + } + if (activeItem) { + input.activeItems = [activeItem]; + } + }); + input.show(); + TPromise.wrap(picks).then(null, err => { + reject(err); + input.hide(); + }); + }); + } + + input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): TPromise { + return new TPromise((resolve, reject) => { + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createInputBox(); + const validateInput = options.validateInput || (() => TPromise.as(undefined)); + const onDidValueChange = debounceEvent(input.onDidChangeValue, (last, cur) => cur, 100); + let validationValue = options.value || ''; + let validation = TPromise.wrap(validateInput(validationValue)); + const disposables = [ + input, + onDidValueChange(value => { + if (value !== validationValue) { + validation = TPromise.wrap(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (value === validationValue) { + input.validationMessage = result; } }); - result.then(null, onUnexpectedError); - return result; - } - } - this.inQuickOpen('quickInput', false); - this.ui.container.style.display = 'none'; - if (!focusLost) { - this.editorGroupService.activeGroup.focus(); - } - return TPromise.as(undefined); + }), + input.onDidAccept(() => { + const value = input.value; + if (value !== validationValue) { + validation = TPromise.wrap(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (!result) { + resolve(value); + input.hide(); + } else if (value === validationValue) { + input.validationMessage = result; + } + }); + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + input.value = options.value; + input.valueSelection = options.valueSelection; + input.prompt = options.prompt; + input.placeholder = options.placeHolder; + input.password = options.password; + input.ignoreFocusOut = options.ignoreFocusLost; + input.show(); + }); } - pick(picks: TPromise, options: O = {}, token?: CancellationToken): TPromise { - return this._pick(undefined, picks, options, token); + backButton = backButton; + + createQuickPick(): IQuickPick { + this.create(); + return new QuickPick(this.ui); } - private _pick(handle: CancellationTokenSource | undefined, picks: TPromise, options: O = {}, token?: CancellationToken): TPromise { - return this._show(handle, { - type: options.canPickMany ? 'pickMany' : 'pickOne', - picks, - placeHolder: options.placeHolder, - matchOnDescription: options.matchOnDescription, - matchOnDetail: options.matchOnDetail, - ignoreFocusLost: options.ignoreFocusLost - }, token); + createInputBox(): IInputBox { + this.create(); + return new InputBox(this.ui); } - input(options: IInputOptions = {}, token?: CancellationToken): TPromise { - return this._input(undefined, options, token); - } - - private _input(handle: CancellationTokenSource | undefined, options: IInputOptions = {}, token?: CancellationToken): TPromise { - return this._show(handle, { - type: 'textInput', - value: options.value, - valueSelection: options.valueSelection, - prompt: options.prompt, - placeHolder: options.placeHolder, - password: options.password, - ignoreFocusLost: options.ignoreFocusLost, - validateInput: options.validateInput, - }, token); - } - - private _show | PickManyParameters>(multiStepHandle: CancellationTokenSource | undefined, parameters: P, token?: CancellationToken): TPromise

? T[] : T>; - private _show(multiStepHandle: CancellationTokenSource | undefined, parameters: TextInputParameters, token?: CancellationToken): TPromise; - private _show(multiStepHandle: CancellationTokenSource | undefined, parameters: InputParameters, token: CancellationToken = CancellationToken.None): TPromise { - if (multiStepHandle && multiStepHandle !== this.multiStepHandle) { - multiStepHandle.cancel(); - return TPromise.as(undefined); - } - if (!multiStepHandle && this.multiStepHandle) { - this.multiStepHandle.cancel(); - } - + private show(controller: QuickInput) { this.create(); this.quickOpenService.close(); - if (this.controller) { - this.controller.resolve(); + const oldController = this.controller; + this.controller = controller; + if (oldController) { + oldController.didHide(); } - this.ui.container.setAttribute('data-type', parameters.type); - - this.ignoreFocusLost = parameters.ignoreFocusLost; - - this.progressBar.stop(); - this.ready = false; - - this.controller = this.createController(parameters); - this.ui.checkAll.style.display = this.controller.showUI.checkAll ? '' : 'none'; - this.filterContainer.style.display = this.controller.showUI.inputBox ? '' : 'none'; + this.setEnabled(true); + this.ui.leftActionBar.clear(); + this.ui.title.textContent = ''; + this.ui.rightActionBar.clear(); + this.ui.checkAll.checked = false; + // this.ui.inputBox.value = ''; Avoid triggering an event. + this.ui.inputBox.placeholder = ''; + this.ui.inputBox.password = false; this.ui.inputBox.showDecoration(Severity.Ignore); - this.countContainer.style.display = this.controller.showUI.count ? '' : 'none'; - this.okContainer.style.display = this.controller.showUI.ok ? '' : 'none'; - this.ui.message.style.display = this.controller.showUI.message ? '' : 'none'; - this.ui.list.display(this.controller.showUI.list); + this.ui.visibleCount.setCount(0); + this.ui.count.setCount(0); + this.ui.message.textContent = ''; + this.ui.progressBar.stop(); + this.ui.list.setElements([]); + this.ui.list.matchOnDescription = false; + this.ui.list.matchOnDetail = false; + this.ui.ignoreFocusOut = false; + this.ui.keyMods.ctrlCmd = false; + this.ui.keyMods.alt = false; + this.setComboboxAccessibility(false); + + const keybinding = this.keybindingService.lookupKeybinding(BackAction.ID); + backButton.tooltip = keybinding ? localize('quickInput.backWithKeybinding', "Back ({0})", keybinding.getLabel()) : localize('quickInput.back', "Back"); + + this.inQuickOpen('quickInput', true); + this.resetContextKeys(); - if (this.ui.container.style.display === 'none') { - this.inQuickOpen('quickInput', true); - } this.ui.container.style.display = ''; this.updateLayout(); this.ui.inputBox.setFocus(); - - const d = token.onCancellationRequested(() => this.close()); - this.controller.result.then(() => d.dispose(), () => d.dispose()); - - const wasController = this.controller; - - const delay = TPromise.timeout(800); - delay.then(() => { - if (this.controller === wasController) { - this.progressBar.infinite(); - } - }, () => { /* ignore */ }); - - this.controller.ready.then(() => { - delay.cancel(); - if (this.controller !== wasController) { - return; - } - - this.progressBar.stop(); - this.ready = true; - - this.updateLayout(); - }).then(null, reason => this.close(TPromise.wrapError(reason))); - - return this.controller.result; } - private createController(parameters: InputParameters) { - switch (parameters.type) { - case 'pickOne': return new PickOneController(this.ui, parameters); - case 'pickMany': return new PickManyController(this.ui, parameters); - case 'textInput': return new TextInputController(this.ui, parameters); - default: ((p: never) => { - throw new Error(`Unknown input type: ${(p).type}`); - })(parameters); + private setVisibilities(visibilities: Visibilities) { + this.ui.title.style.display = visibilities.title ? '' : 'none'; + this.ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; + this.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; + this.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; + this.countContainer.style.display = visibilities.count ? '' : 'none'; + this.okContainer.style.display = visibilities.ok ? '' : 'none'; + this.ui.message.style.display = visibilities.message ? '' : 'none'; + this.ui.list.display(visibilities.list); + this.ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes'); + this.updateLayout(); // TODO + } + + private setComboboxAccessibility(enabled: boolean) { + if (enabled !== this.comboboxAccessibility) { + this.comboboxAccessibility = enabled; + if (this.comboboxAccessibility) { + this.ui.inputBox.setAttribute('role', 'combobox'); + this.ui.inputBox.setAttribute('aria-haspopup', 'true'); + this.ui.inputBox.setAttribute('aria-autocomplete', 'list'); + this.ui.inputBox.setAttribute('aria-activedescendant', this.ui.list.getActiveDescendant()); + } else { + this.ui.inputBox.removeAttribute('role'); + this.ui.inputBox.removeAttribute('aria-haspopup'); + this.ui.inputBox.removeAttribute('aria-autocomplete'); + this.ui.inputBox.removeAttribute('aria-activedescendant'); + } } } - multiStepInput(handler: (input: IQuickInput, token: CancellationToken) => Thenable, token = CancellationToken.None): Thenable { - if (this.multiStepHandle) { - this.multiStepHandle.cancel(); + private isScreenReaderOptimized() { + const detected = browser.getAccessibilitySupport() === AccessibilitySupport.Enabled; + const config = this.configurationService.getValue('editor').accessibilitySupport; + return config === 'on' || (config === 'auto' && detected); + } + + private setEnabled(enabled: boolean) { + if (enabled !== this.enabled) { + this.enabled = enabled; + for (const item of this.ui.leftActionBar.items) { + (item as ActionItem).getAction().enabled = enabled; + } + for (const item of this.ui.rightActionBar.items) { + (item as ActionItem).getAction().enabled = enabled; + } + this.ui.checkAll.disabled = !enabled; + // this.ui.inputBox.enabled = enabled; Avoid loosing focus. + this.ok.enabled = enabled; + this.ui.list.enabled = enabled; + } + } + + private hide(focusLost?: boolean) { + const controller = this.controller; + if (controller) { + this.controller = null; + this.inQuickOpen('quickInput', false); + this.resetContextKeys(); + this.ui.container.style.display = 'none'; + if (!focusLost) { + this.editorGroupService.activeGroup.focus(); + } + controller.didHide(); } - this.multiStepHandle = new CancellationTokenSource(); - return TPromise.wrap(handler({ - pick: this._pick.bind(this, this.multiStepHandle), - input: this._input.bind(this, this.multiStepHandle) - }, this.multiStepHandle.token)); } focus() { @@ -678,7 +1308,7 @@ export class QuickInputService extends Component implements IQuickInputService { } toggle() { - if (this.isDisplayed() && this.controller instanceof PickManyController) { + if (this.isDisplayed() && this.controller instanceof QuickPick && this.controller.canSelectMany) { this.ui.list.toggleCheckbox(); } } @@ -686,18 +1316,25 @@ export class QuickInputService extends Component implements IQuickInputService { navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { if (this.isDisplayed() && this.ui.list.isDisplayed()) { this.ui.list.focus(next ? 'Next' : 'Previous'); - if (quickNavigate && this.controller instanceof PickOneController) { - this.controller.configureQuickNavigate(quickNavigate); + if (quickNavigate && this.controller instanceof QuickPick) { + this.controller.quickNavigate = quickNavigate; } } } accept() { - return this.close(true); + this.onDidAcceptEmitter.fire(); + return TPromise.as(undefined); + } + + back() { + this.onDidTriggerButtonEmitter.fire(this.backButton); + return TPromise.as(undefined); } cancel() { - return this.close(); + this.hide(); + return TPromise.as(undefined); } layout(dimension: dom.Dimension): void { @@ -723,9 +1360,10 @@ export class QuickInputService extends Component implements IQuickInputService { protected updateStyles() { const theme = this.themeService.getTheme(); if (this.ui) { + // TODO + const titleColor = { dark: 'rgba(255, 255, 255, 0.105)', light: 'rgba(0,0,0,.06)', hc: 'black' }[theme.type]; + this.titleBar.style.backgroundColor = titleColor ? titleColor.toString() : undefined; this.ui.inputBox.style(theme); - } - if (this.ui) { const sideBarBackground = theme.getColor(SIDE_BAR_BACKGROUND); this.ui.container.style.backgroundColor = sideBarBackground ? sideBarBackground.toString() : undefined; const sideBarForeground = theme.getColor(SIDE_BAR_FOREGROUND); @@ -744,7 +1382,7 @@ export class QuickInputService extends Component implements IQuickInputService { export const QuickPickManyToggle: ICommandAndKeybindingRule = { id: 'workbench.action.quickPickManyToggle', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: inQuickOpenContext, primary: undefined, handler: accessor => { @@ -752,3 +1390,18 @@ export const QuickPickManyToggle: ICommandAndKeybindingRule = { quickInputService.toggle(); } }; + +export class BackAction extends Action { + + public static readonly ID = 'workbench.action.quickInputBack'; + public static readonly LABEL = localize('back', "Back"); + + constructor(id: string, label: string, @IQuickInputService private quickInputService: IQuickInputService) { + super(id, label); + } + + public run(): TPromise { + this.quickInputService.back(); + return TPromise.as(null); + } +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts index b99245aece5..5ec8b988067 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -9,7 +9,7 @@ import 'vs/css!./quickInput'; import * as dom from 'vs/base/browser/dom'; import { InputBox, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; -import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; import { ITheme } from 'vs/platform/theme/common/themeService'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -33,12 +33,6 @@ export class QuickInputBox { ariaLabel: DEFAULT_INPUT_ARIA_LABEL }); this.disposables.push(this.inputBox); - - // ARIA - const inputElement = this.inputBox.inputElement; - inputElement.setAttribute('role', 'combobox'); - inputElement.setAttribute('aria-haspopup', 'false'); - inputElement.setAttribute('aria-autocomplete', 'list'); } onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => { @@ -67,8 +61,32 @@ export class QuickInputBox { this.inputBox.setPlaceHolder(placeholder); } - setPassword(isPassword: boolean): void { - this.inputBox.inputElement.type = isPassword ? 'password' : 'text'; + get placeholder() { + return this.inputBox.inputElement.getAttribute('placeholder'); + } + + set placeholder(placeholder: string) { + this.inputBox.setPlaceHolder(placeholder); + } + + get password() { + return this.inputBox.inputElement.type === 'password'; + } + + set password(password: boolean) { + this.inputBox.inputElement.type = password ? 'password' : 'text'; + } + + set enabled(enabled: boolean) { + this.inputBox.setEnabled(enabled); + } + + setAttribute(name: string, value: string) { + this.inputBox.inputElement.setAttribute(name, value); + } + + removeAttribute(name: string) { + this.inputBox.inputElement.removeAttribute(name); } showDecoration(decoration: Severity): void { @@ -93,10 +111,13 @@ export class QuickInputBox { inputBackground: theme.getColor(inputBackground), inputBorder: theme.getColor(inputBorder), inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), }); } diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts index 1af9308ea94..3bb2a9c4d12 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts @@ -6,12 +6,12 @@ 'use strict'; import 'vs/css!./quickInput'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import * as dom from 'vs/base/browser/dom'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPickOpenEntry } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IMatch } from 'vs/base/common/filters'; import { matchesFuzzyOcticonAware, parseOcticons } from 'vs/base/common/octicon'; import { compareAnything } from 'vs/base/common/comparers'; @@ -24,20 +24,31 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { memoize } from 'vs/base/common/decorators'; import { range } from 'vs/base/common/arrays'; import * as platform from 'vs/base/common/platform'; -import { listFocusBackground } from 'vs/platform/theme/common/colorRegistry'; +import { listFocusBackground, pickerGroupBorder, pickerGroupForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { getIconClass } from 'vs/workbench/browser/parts/quickinput/quickInputUtils'; const $ = dom.$; interface IListElement { index: number; - item: IPickOpenEntry; - checked?: boolean; + item: IQuickPickItem; + saneLabel: string; + saneDescription?: string; + saneDetail?: string; + checked: boolean; + separator: IQuickPickSeparator; + fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; } class ListElement implements IListElement { index: number; - item: IPickOpenEntry; + item: IQuickPickItem; + saneLabel: string; + saneDescription?: string; + saneDetail?: string; shouldAlwaysShow = false; hidden = false; private _onChecked = new Emitter(); @@ -52,9 +63,11 @@ class ListElement implements IListElement { this._onChecked.fire(value); } } + separator: IQuickPickSeparator; labelHighlights?: IMatch[]; descriptionHighlights?: IMatch[]; detailHighlights?: IMatch[]; + fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; constructor(init: IListElement) { assign(this, init); @@ -62,9 +75,12 @@ class ListElement implements IListElement { } interface IListElementTemplateData { + entry: HTMLDivElement; checkbox: HTMLInputElement; label: IconLabel; detail: HighlightedLabel; + separator: HTMLDivElement; + actionBar: ActionBar; element: ListElement; toDisposeElement: IDisposable[]; toDisposeTemplate: IDisposable[]; @@ -83,10 +99,10 @@ class ListElementRenderer implements IRendererdom.append(label, $('input.quick-input-list-checkbox')); data.checkbox.type = 'checkbox'; data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { @@ -105,31 +121,77 @@ class ListElementRenderer implements IRenderer data.checkbox.checked = checked)); - } + data.checkbox.checked = element.checked; + data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked)); const { labelHighlights, descriptionHighlights, detailHighlights } = element; // Label const options: IIconLabelValueOptions = Object.create(null); options.matches = labelHighlights || []; - options.descriptionTitle = element.item.description; + options.descriptionTitle = element.saneDescription; options.descriptionMatches = descriptionHighlights || []; - data.label.setValue(element.item.label, element.item.description, options); + options.extraClasses = element.item.iconClasses; + data.label.setValue(element.saneLabel, element.saneDescription, options); // Meta - data.detail.set(element.item.detail, detailHighlights); + data.detail.set(element.saneDetail, detailHighlights); + + // ARIA label + data.entry.setAttribute('aria-label', [element.saneLabel, element.saneDescription, element.saneDetail] + .filter(s => !!s) + .join(', ')); + + // Separator + if (element.separator && element.separator.label) { + data.separator.textContent = element.separator.label; + data.separator.style.display = null; + } else { + data.separator.style.display = 'none'; + } + if (element.separator) { + dom.addClass(data.entry, 'quick-input-list-separator-border'); + } else { + dom.removeClass(data.entry, 'quick-input-list-separator-border'); + } + + // Actions + data.actionBar.clear(); + const buttons = element.item.buttons; + if (buttons && buttons.length) { + data.actionBar.push(buttons.map((button, index) => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { + element.fireButtonTriggered({ + button, + item: element.item + }); + return null; + }); + action.tooltip = button.tooltip; + return action; + }), { icon: true, label: false }); + dom.addClass(data.entry, 'has-actions'); + } else { + dom.removeClass(data.entry, 'has-actions'); + } + } + + disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void { + data.toDisposeElement = dispose(data.toDisposeElement); } disposeTemplate(data: IListElementTemplateData): void { @@ -138,10 +200,10 @@ class ListElementRenderer implements IRenderer { +class ListElementDelegate implements IVirtualDelegate { getHeight(element: ListElement): number { - return element.item.detail ? 44 : 22; + return element.saneDetail ? 44 : 22; } getTemplateId(element: ListElement): string { @@ -151,15 +213,24 @@ class ListElementDelegate implements IDelegate { export class QuickInputList { + readonly id: string; private container: HTMLElement; private list: WorkbenchList; + private inputElements: (IQuickPickItem | IQuickPickSeparator)[]; private elements: ListElement[] = []; + private elementsToIndexes = new Map(); matchOnDescription = false; matchOnDetail = false; - private _onAllVisibleCheckedChanged = new Emitter(); // TODO: Debounce - onAllVisibleCheckedChanged: Event = this._onAllVisibleCheckedChanged.event; - private _onCheckedCountChanged = new Emitter(); // TODO: Debounce - onCheckedCountChanged: Event = this._onCheckedCountChanged.event; + private _onChangedAllVisibleChecked = new Emitter(); + onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; + private _onChangedCheckedCount = new Emitter(); + onChangedCheckedCount: Event = this._onChangedCheckedCount.event; + private _onChangedVisibleCount = new Emitter(); + onChangedVisibleCount: Event = this._onChangedVisibleCount.event; + private _onChangedCheckedElements = new Emitter(); + onChangedCheckedElements: Event = this._onChangedCheckedElements.event; + private _onButtonTriggered = new Emitter>(); + onButtonTriggered = this._onButtonTriggered.event; private _onLeave = new Emitter(); onLeave: Event = this._onLeave.event; private _fireCheckedEvents = true; @@ -168,14 +239,18 @@ export class QuickInputList { constructor( private parent: HTMLElement, + id: string, @IInstantiationService private instantiationService: IInstantiationService ) { + this.id = id; this.container = dom.append(this.parent, $('.quick-input-list')); const delegate = new ListElementDelegate(); this.list = this.instantiationService.createInstance(WorkbenchList, this.container, delegate, [new ListElementRenderer()], { identityProvider: element => element.label, + openController: { shouldOpen: () => false }, // Workaround #58124 multipleSelectionSupport: false }) as WorkbenchList; + this.list.getHTMLElement().id = id; this.disposables.push(this.list); this.disposables.push(this.list.onKeyDown(e => { const event = new StandardKeyboardEvent(e); @@ -189,12 +264,14 @@ export class QuickInputList { } break; case KeyCode.UpArrow: + case KeyCode.PageUp: const focus1 = this.list.getFocus(); if (focus1.length === 1 && focus1[0] === 0) { this._onLeave.fire(); } break; case KeyCode.DownArrow: + case KeyCode.PageDown: const focus2 = this.list.getFocus(); if (focus2.length === 1 && focus2[0] === this.list.length - 1) { this._onLeave.fire(); @@ -207,20 +284,15 @@ export class QuickInputList { this._onLeave.fire(); } })); - this.disposables.push(this.list.onSelectionChange(e => { - if (e.elements.length) { - this.list.setSelection([]); - } - })); } @memoize - get onFocusChange() { + get onDidChangeFocus() { return mapEvent(this.list.onFocusChange, e => e.elements.map(e => e.item)); } @memoize - get onSelectionChange() { + get onDidChangeSelection() { return mapEvent(this.list.onSelectionChange, e => e.elements.map(e => e.item)); } @@ -253,6 +325,17 @@ export class QuickInputList { return count; } + getVisibleCount() { + let count = 0; + const elements = this.elements; + for (let i = 0, n = elements.length; i < n; i++) { + if (!elements[i].hidden) { + count++; + } + } + return count; + } + setAllVisibleChecked(checked: boolean) { try { this._fireCheckedEvents = false; @@ -267,23 +350,35 @@ export class QuickInputList { } } - setElements(elements: IPickOpenEntry[], canCheck = false): void { + setElements(inputElements: (IQuickPickItem | IQuickPickSeparator)[]): void { this.elementDisposables = dispose(this.elementDisposables); - this.elements = elements.map((item, index) => new ListElement({ - index, - item, - checked: canCheck ? !!item.picked : undefined - })); - if (canCheck) { - this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents()))); - } + const fireButtonTriggered = (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event); + this.inputElements = inputElements; + this.elements = inputElements.reduce((result, item, index) => { + if (item.type !== 'separator') { + const previous = index && inputElements[index - 1]; + result.push(new ListElement({ + index, + item, + saneLabel: item.label && item.label.replace(/\r?\n/g, ' '), + saneDescription: item.description && item.description.replace(/\r?\n/g, ' '), + saneDetail: item.detail && item.detail.replace(/\r?\n/g, ' '), + checked: false, + separator: previous && previous.type === 'separator' ? previous : undefined, + fireButtonTriggered + })); + } + return result; + }, [] as ListElement[]); + this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents()))); + + this.elementsToIndexes = this.elements.reduce((map, element, index) => { + map.set(element.item, index); + return map; + }, new Map()); this.list.splice(0, this.list.length, this.elements); this.list.setFocus([]); - } - - getCheckedElements() { - return this.elements.filter(e => e.checked) - .map(e => e.item); + this._onChangedVisibleCount.fire(this.elements.length); } getFocusedElements() { @@ -291,15 +386,61 @@ export class QuickInputList { .map(e => e.item); } + setFocusedElements(items: IQuickPickItem[]) { + this.list.setFocus(items + .filter(item => this.elementsToIndexes.has(item)) + .map(item => this.elementsToIndexes.get(item))); + } + + getActiveDescendant() { + return this.list.getHTMLElement().getAttribute('aria-activedescendant'); + } + + getSelectedElements() { + return this.list.getSelectedElements() + .map(e => e.item); + } + + setSelectedElements(items: IQuickPickItem[]) { + this.list.setSelection(items + .filter(item => this.elementsToIndexes.has(item)) + .map(item => this.elementsToIndexes.get(item))); + } + + getCheckedElements() { + return this.elements.filter(e => e.checked) + .map(e => e.item); + } + + setCheckedElements(items: IQuickPickItem[]) { + try { + this._fireCheckedEvents = false; + const checked = new Set(); + for (const item of items) { + checked.add(item); + } + for (const element of this.elements) { + element.checked = checked.has(element.item); + } + } finally { + this._fireCheckedEvents = true; + this.fireCheckedEvents(); + } + } + + set enabled(value: boolean) { + this.list.getHTMLElement().style.pointerEvents = value ? null : 'none'; + } + focus(what: 'First' | 'Last' | 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void { if (!this.list.length) { return; } - if (what === 'Next' && this.list.getFocus()[0] === this.list.length - 1) { + if ((what === 'Next' || what === 'NextPage') && this.list.getFocus()[0] === this.list.length - 1) { what = 'First'; } - if (what === 'Previous' && this.list.getFocus()[0] === 0) { + if ((what === 'Previous' || what === 'PreviousPage') && this.list.getFocus()[0] === 0) { what = 'Last'; } @@ -329,15 +470,17 @@ export class QuickInputList { element.descriptionHighlights = undefined; element.detailHighlights = undefined; element.hidden = false; + const previous = element.index && this.inputElements[element.index - 1]; + element.separator = previous && previous.type === 'separator' ? previous : undefined; }); } // Filter by value (since we support octicons, use octicon aware fuzzy matching) else { this.elements.forEach(element => { - const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.item.label)); - const descriptionHighlights = this.matchOnDescription ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.description || '')) : undefined; - const detailHighlights = this.matchOnDetail ? matchesFuzzyOcticonAware(query, parseOcticons(element.item.detail || '')) : undefined; + const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.saneLabel)); + const descriptionHighlights = this.matchOnDescription ? matchesFuzzyOcticonAware(query, parseOcticons(element.saneDescription || '')) : undefined; + const detailHighlights = this.matchOnDetail ? matchesFuzzyOcticonAware(query, parseOcticons(element.saneDetail || '')) : undefined; if (element.shouldAlwaysShow || labelHighlights || descriptionHighlights || detailHighlights) { element.labelHighlights = labelHighlights; @@ -348,27 +491,32 @@ export class QuickInputList { element.labelHighlights = undefined; element.descriptionHighlights = undefined; element.detailHighlights = undefined; - element.hidden = true; + element.hidden = !element.item.alwaysShow; } + element.separator = undefined; }); } const shownElements = this.elements.filter(element => !element.hidden); // Sort by value - const normalizedSearchValue = query.toLowerCase(); - shownElements.sort((a, b) => { - if (!query) { - return a.index - b.index; // restore natural order - } - return compareEntries(a, b, normalizedSearchValue); - }); + if (query) { + const normalizedSearchValue = query.toLowerCase(); + shownElements.sort((a, b) => { + return compareEntries(a, b, normalizedSearchValue); + }); + } + this.elementsToIndexes = shownElements.reduce((map, element, index) => { + map.set(element.item, index); + return map; + }, new Map()); this.list.splice(0, this.list.length, shownElements); this.list.setFocus([]); this.list.layout(); - this._onAllVisibleCheckedChanged.fire(this.getAllVisibleChecked()); + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedVisibleCount.fire(shownElements.length); } toggleCheckbox() { @@ -400,10 +548,15 @@ export class QuickInputList { private fireCheckedEvents() { if (this._fireCheckedEvents) { - this._onAllVisibleCheckedChanged.fire(this.getAllVisibleChecked()); - this._onCheckedCountChanged.fire(this.getCheckedCount()); + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedCheckedCount.fire(this.getCheckedCount()); + this._onChangedCheckedElements.fire(this.getCheckedElements()); } } + + private fireButtonTriggered(event: IQuickPickItemButtonEvent) { + this._onButtonTriggered.fire(event); + } } function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number { @@ -418,7 +571,7 @@ function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: s return 1; } - return compareAnything(elementA.item.label, elementB.item.label, lookFor); + return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor); } registerThemingParticipant((theme, collector) => { @@ -428,4 +581,12 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused { background-color: ${listInactiveFocusBackground}; }`); collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused:hover { background-color: ${listInactiveFocusBackground}; }`); } + const pickerGroupBorderColor = theme.getColor(pickerGroupBorder); + if (pickerGroupBorderColor) { + collector.addRule(`.quick-input-list .quick-input-list-entry { border-top-color: ${pickerGroupBorderColor}; }`); + } + const pickerGroupForegroundColor = theme.getColor(pickerGroupForeground); + if (pickerGroupForegroundColor) { + collector.addRule(`.quick-input-list .quick-input-list-separator { color: ${pickerGroupForegroundColor}; }`); + } }); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts new file mode 100644 index 00000000000..1feda0eee00 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./quickInput'; +import * as dom from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { IdGenerator } from 'vs/base/common/idGenerator'; + +const iconPathToClass = {}; +const iconClassGenerator = new IdGenerator('quick-input-button-icon-'); + +export function getIconClass(iconPath: { dark: URI; light?: URI; }) { + let iconClass: string; + + const key = iconPath.dark.toString(); + if (iconPathToClass[key]) { + iconClass = iconPathToClass[key]; + } else { + iconClass = iconClassGenerator.nextId(); + dom.createCSSRule(`.${iconClass}`, `background-image: url("${(iconPath.light || iconPath.dark).toString()}")`); + dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: url("${iconPath.dark.toString()}")`); + iconPathToClass[key] = iconClass; + } + + return iconClass; +} diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index e19932ffc94..d3d4c3be25a 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -10,18 +10,15 @@ import { TPromise, ValueCallback } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; -import { defaultGenerator } from 'vs/base/common/idGenerator'; import * as types from 'vs/base/common/types'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action } from 'vs/base/common/actions'; import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; -import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, compareEntries, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { QuickOpenWidget, HideReason } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; import { ContributableActionProvider } from 'vs/workbench/browser/actions'; -import * as labels from 'vs/base/common/labels'; import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; import { Registry } from 'vs/platform/registry/common/platform'; import { IResourceInput } from 'vs/platform/editor/common/editor'; @@ -34,74 +31,56 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG } from 'vs/workbench/browser/quickopen'; import * as errors from 'vs/base/common/errors'; -import { IPickOpenEntry, IFilePickOpenEntry, IQuickOpenService, IPickOptions, IShowOptions, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen'; +import { IQuickOpenService, IShowOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; -import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { FileKind, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; -import { matchesFuzzyOcticonAware, parseOcticons, IParsedOcticons } from 'vs/base/common/octicon'; -import { IMatch } from 'vs/base/common/filters'; import { Schemas } from 'vs/base/common/network'; -import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, addClass } from 'vs/base/browser/dom'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { timeout } from 'vs/base/common/async'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; const HELP_PREFIX = '?'; -interface IInternalPickOptions { - contextKey?: string; - value?: string; - valueSelection?: [number, number]; - placeHolder?: string; - inputDecoration?: Severity; - password?: boolean; - autoFocus?: IAutoFocus; - matchOnDescription?: boolean; - matchOnDetail?: boolean; - ignoreFocusLost?: boolean; - quickNavigateConfiguration?: IQuickNavigateConfiguration; - onDidType?: (value: string) => any; -} - export class QuickOpenController extends Component implements IQuickOpenService { private static readonly MAX_SHORT_RESPONSE_TIME = 500; - - public _serviceBrand: any; - private static readonly ID = 'workbench.component.quickopen'; - private readonly _onShow: Emitter; - private readonly _onHide: Emitter; + _serviceBrand: any; + + private readonly _onShow: Emitter = this._register(new Emitter()); + get onShow(): Event { return this._onShow.event; } + + private readonly _onHide: Emitter = this._register(new Emitter()); + get onHide(): Event { return this._onHide.event; } private quickOpenWidget: QuickOpenWidget; - private pickOpenWidget: QuickOpenWidget; - private layoutDimensions: Dimension; - private mapResolvedHandlersToPrefix: { [prefix: string]: TPromise; }; - private mapContextKeyToContext: { [id: string]: IContextKey; }; - private handlerOnOpenCalled: { [prefix: string]: boolean; }; - private currentResultToken: string; - private currentPickerToken: string; - private promisesToCompleteOnHide: ValueCallback[]; + private dimension: Dimension; + private mapResolvedHandlersToPrefix: { [prefix: string]: TPromise; } = Object.create(null); + private mapContextKeyToContext: { [id: string]: IContextKey; } = Object.create(null); + private handlerOnOpenCalled: { [prefix: string]: boolean; } = Object.create(null); + private promisesToCompleteOnHide: ValueCallback[] = []; private previousActiveHandlerDescriptor: QuickOpenHandlerDescriptor; private actionProvider = new ContributableActionProvider(); private closeOnFocusLost: boolean; private editorHistoryHandler: EditorHistoryHandler; + private pendingGetResultsInvocation: CancellationTokenSource; constructor( - @IEditorService private editorService: IEditorService, @IEditorGroupsService private editorGroupService: IEditorGroupsService, @INotificationService private notificationService: INotificationService, @IContextKeyService private contextKeyService: IContextKeyService, @@ -113,26 +92,17 @@ export class QuickOpenController extends Component implements IQuickOpenService ) { super(QuickOpenController.ID, themeService); - this.mapResolvedHandlersToPrefix = {}; - this.handlerOnOpenCalled = {}; - this.mapContextKeyToContext = {}; - - this.promisesToCompleteOnHide = []; - this.editorHistoryHandler = this.instantiationService.createInstance(EditorHistoryHandler); - this._onShow = new Emitter(); - this._onHide = new Emitter(); - this.updateConfiguration(); this.registerListeners(); } private registerListeners(): void { - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.updateConfiguration())); - this.toUnbind.push(this.partService.onTitleBarVisibilityChange(() => this.positionQuickOpenWidget())); - this.toUnbind.push(browser.onDidChangeZoomLevel(() => this.positionQuickOpenWidget())); + this._register(this.configurationService.onDidChangeConfiguration(() => this.updateConfiguration())); + this._register(this.partService.onTitleBarVisibilityChange(() => this.positionQuickOpenWidget())); + this._register(browser.onDidChangeZoomLevel(() => this.positionQuickOpenWidget())); } private updateConfiguration(): void { @@ -143,300 +113,28 @@ export class QuickOpenController extends Component implements IQuickOpenService } } - public get onShow(): Event { - return this._onShow.event; - } - - public get onHide(): Event { - return this._onHide.event; - } - - public navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { + navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { if (this.quickOpenWidget) { this.quickOpenWidget.navigate(next, quickNavigate); } + } - if (this.pickOpenWidget) { - this.pickOpenWidget.navigate(next, quickNavigate); + accept(): void { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.accept(); } } - public pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - public pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; - public pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; - public pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; - public pick(arg1: string[] | TPromise | IPickOpenEntry[] | TPromise, options?: IPickOptions, token?: CancellationToken): TPromise { - if (!options) { - options = Object.create(null); + focus(): void { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.focus(); } - - let arrayPromise: TPromise; - if (Array.isArray(arg1)) { - arrayPromise = TPromise.as(arg1); - } else if (TPromise.is(arg1)) { - arrayPromise = arg1; - } else { - throw new Error('illegal input'); - } - - let isAboutStrings = false; - const entryPromise = arrayPromise.then(elements => { - return (>elements).map(element => { - if (typeof element === 'string') { - isAboutStrings = true; - - return { label: element }; - } else { - return element; - } - }); - }); - - if (this.pickOpenWidget && this.pickOpenWidget.isVisible()) { - this.pickOpenWidget.hide(HideReason.CANCELED); - } - - return new TPromise((resolve, reject, progress) => { - - function onItem(item: IPickOpenEntry): string | IPickOpenEntry { - return item && isAboutStrings ? item.label : item; - } - - this.doPick(entryPromise, options, token).then(item => resolve(onItem(item)), err => reject(err), item => progress(onItem(item))); - }); } - private doPick(picksPromise: TPromise, options: IInternalPickOptions, token: CancellationToken = CancellationToken.None): TPromise { - const autoFocus = options.autoFocus; - - // Use a generated token to avoid race conditions from long running promises - const currentPickerToken = defaultGenerator.nextId(); - this.currentPickerToken = currentPickerToken; - - // Update context - this.setQuickOpenContextKey(options.contextKey); - - // Create upon first open - if (!this.pickOpenWidget) { - this.pickOpenWidget = new QuickOpenWidget( - document.getElementById(this.partService.getWorkbenchElementId()), - { - onOk: () => { /* ignore, handle later */ }, - onCancel: () => { /* ignore, handle later */ }, - onType: (value: string) => { /* ignore, handle later */ }, - onShow: () => this.handleOnShow(true), - onHide: (reason) => this.handleOnHide(true, reason) - }, { - inputPlaceHolder: options.placeHolder || '', - keyboardSupport: false, - treeCreator: (container, config, opts) => this.instantiationService.createInstance(WorkbenchTree, container, config, opts) - } - ); - this.toUnbind.push(attachQuickOpenStyler(this.pickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND })); - - const pickOpenContainer = this.pickOpenWidget.create(); - addClass(pickOpenContainer, 'show-file-icons'); - this.positionQuickOpenWidget(); + close(): void { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.hide(HideReason.CANCELED); } - - // Update otherwise - else { - this.pickOpenWidget.setPlaceHolder(options.placeHolder || ''); - } - - // Respect input value - if (options.value) { - this.pickOpenWidget.setValue(options.value, options.valueSelection); - } - - // Respect password - this.pickOpenWidget.setPassword(options.password); - - // Input decoration - if (!types.isUndefinedOrNull(options.inputDecoration)) { - this.pickOpenWidget.showInputDecoration(options.inputDecoration); - } else { - this.pickOpenWidget.clearInputDecoration(); - } - - // Layout - if (this.layoutDimensions) { - this.pickOpenWidget.layout(this.layoutDimensions); - } - - return new TPromise((complete, error, progress) => { - - // Detect cancellation while pick promise is loading - this.pickOpenWidget.setCallbacks({ - onCancel: () => { complete(void 0); }, - onOk: () => { /* ignore, handle later */ }, - onType: (value: string) => { /* ignore, handle later */ }, - }); - - // hide widget when being cancelled - token.onCancellationRequested(e => { - if (this.currentPickerToken === currentPickerToken) { - this.pickOpenWidget.hide(HideReason.CANCELED); - } - }); - - let picksPromiseDone = false; - - // Resolve picks - picksPromise.then(picks => { - if (this.currentPickerToken !== currentPickerToken) { - return complete(void 0); // Return as canceled if another request came after or user canceled - } - - picksPromiseDone = true; - - // Reset Progress - this.pickOpenWidget.getProgressBar().stop().hide(); - - // Model - const model = new QuickOpenModel([], new PickOpenActionProvider()); - const entries = picks.map((e, index) => this.instantiationService.createInstance(PickOpenEntry, e, index, () => progress(e), () => this.pickOpenWidget.refresh())); - if (picks.length === 0) { - entries.push(this.instantiationService.createInstance(PickOpenEntry, { label: nls.localize('emptyPicks', "There are no entries to pick from") }, 0, null, null)); - } - - model.setEntries(entries); - - // Handlers - const callbacks = { - onOk: () => { - if (picks.length === 0) { - return complete(null); - } - - let index = -1; - let context: IEntryRunContext; - entries.forEach(entry => { - if (entry.shouldRunWithContext) { - index = entry.index; - context = entry.shouldRunWithContext; - } - }); - - const selectedPick = picks[index]; - - if (selectedPick && typeof selectedPick.run === 'function') { - selectedPick.run(context); - } - - complete(selectedPick || null); - }, - onCancel: () => complete(void 0), - onFocusLost: () => !this.closeOnFocusLost || options.ignoreFocusLost, - onType: (value: string) => { - - // the caller takes care of all input - if (options.onDidType) { - options.onDidType(value); - return; - } - - if (picks.length === 0) { - return; - } - - value = value ? strings.trim(value) : value; - - // Reset filtering - if (!value) { - entries.forEach(e => { - e.setHighlights(null); - e.setHidden(false); - }); - } - - // Filter by value (since we support octicons, use octicon aware fuzzy matching) - else { - entries.forEach(entry => { - const { labelHighlights, descriptionHighlights, detailHighlights } = entry.matchesFuzzy(value, options); - - if (entry.shouldAlwaysShow() || labelHighlights || descriptionHighlights || detailHighlights) { - entry.setHighlights(labelHighlights, descriptionHighlights, detailHighlights); - entry.setHidden(false); - } else { - entry.setHighlights(null, null, null); - entry.setHidden(true); - } - }); - } - - // Sort by value - const normalizedSearchValue = value ? strings.stripWildcards(value.toLowerCase()) : value; - model.entries.sort((pickA: PickOpenEntry, pickB: PickOpenEntry) => { - if (!value) { - return pickA.index - pickB.index; // restore natural order - } - - return compareEntries(pickA, pickB, normalizedSearchValue); - }); - - this.pickOpenWidget.refresh(model, value ? { autoFocusFirstEntry: true } : autoFocus); - }, - onShow: () => this.handleOnShow(true), - onHide: (reason: HideReason) => this.handleOnHide(true, reason) - }; - this.pickOpenWidget.setCallbacks(callbacks); - - // Set input - if (!this.pickOpenWidget.isVisible()) { - this.pickOpenWidget.show(model, { autoFocus, quickNavigateConfiguration: options.quickNavigateConfiguration }); - } else { - this.pickOpenWidget.setInput(model, autoFocus); - } - - // The user might have typed something (or options.value was set) so we need to play back - // the input box value through our callbacks to filter the result accordingly. - const inputValue = this.pickOpenWidget.getInputBox().value; - if (inputValue) { - callbacks.onType(inputValue); - } - }, (err) => { - this.pickOpenWidget.hide(); - - error(err); - }); - - // Progress if task takes a long time - TPromise.timeout(800).then(() => { - if (!picksPromiseDone && this.currentPickerToken === currentPickerToken) { - this.pickOpenWidget.getProgressBar().infinite().show(); - } - }); - - // Show picker empty if resolving takes a while - if (!picksPromiseDone) { - this.pickOpenWidget.show(new QuickOpenModel()); - } - }); - } - - public accept(): void { - [this.quickOpenWidget, this.pickOpenWidget].forEach(w => { - if (w && w.isVisible()) { - w.accept(); - } - }); - } - - public focus(): void { - [this.quickOpenWidget, this.pickOpenWidget].forEach(w => { - if (w && w.isVisible()) { - w.focus(); - } - }); - } - - public close(): void { - [this.quickOpenWidget, this.pickOpenWidget].forEach(w => { - if (w && w.isVisible()) { - w.hide(HideReason.CANCELED); - } - }); } private emitQuickOpenVisibilityChange(isVisible: boolean): void { @@ -447,7 +145,7 @@ export class QuickOpenController extends Component implements IQuickOpenService } } - public show(prefix?: string, options?: IShowOptions): TPromise { + show(prefix?: string, options?: IShowOptions): TPromise { let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0; let inputSelection = options ? options.inputSelection : void 0; let autoFocus = options ? options.autoFocus : void 0; @@ -461,26 +159,26 @@ export class QuickOpenController extends Component implements IQuickOpenService const handlerDescriptor = registry.getQuickOpenHandler(prefix) || registry.getDefaultQuickOpenHandler(); // Trigger onOpen - this.resolveHandler(handlerDescriptor).done(null, errors.onUnexpectedError); + this.resolveHandler(handlerDescriptor); // Create upon first open if (!this.quickOpenWidget) { - this.quickOpenWidget = new QuickOpenWidget( - document.getElementById(this.partService.getWorkbenchElementId()), + this.quickOpenWidget = this._register(new QuickOpenWidget( + this.partService.getWorkbenchElement(), { onOk: () => { /* ignore */ }, onCancel: () => { /* ignore */ }, onType: (value: string) => this.onType(value || ''), - onShow: () => this.handleOnShow(false), - onHide: (reason) => this.handleOnHide(false, reason), + onShow: () => this.handleOnShow(), + onHide: (reason) => this.handleOnHide(reason), onFocusLost: () => !this.closeOnFocusLost }, { inputPlaceHolder: this.hasHandler(HELP_PREFIX) ? nls.localize('quickOpenInput', "Type '?' to get help on the actions you can take from here") : '', keyboardSupport: false, treeCreator: (container, config, opts) => this.instantiationService.createInstance(WorkbenchTree, container, config, opts) } - ); - this.toUnbind.push(attachQuickOpenStyler(this.quickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND })); + )); + this._register(attachQuickOpenStyler(this.quickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND })); const quickOpenContainer = this.quickOpenWidget.create(); addClass(quickOpenContainer, 'show-file-icons'); @@ -488,8 +186,8 @@ export class QuickOpenController extends Component implements IQuickOpenService } // Layout - if (this.layoutDimensions) { - this.quickOpenWidget.layout(this.layoutDimensions); + if (this.dimension) { + this.quickOpenWidget.layout(this.dimension); } // Show quick open with prefix or editor history @@ -507,8 +205,8 @@ export class QuickOpenController extends Component implements IQuickOpenService if (!quickNavigateConfiguration) { autoFocus = { autoFocusFirstEntry: true }; } else { - const visibleEditorCount = this.editorService.visibleEditors.length; - autoFocus = { autoFocusFirstEntry: visibleEditorCount === 0, autoFocusSecondEntry: visibleEditorCount !== 0 }; + const autoFocusFirstEntry = this.editorGroupService.activeGroup.count === 0; + autoFocus = { autoFocusFirstEntry, autoFocusSecondEntry: !autoFocusFirstEntry }; } } @@ -534,44 +232,33 @@ export class QuickOpenController extends Component implements IQuickOpenService if (this.quickOpenWidget) { this.quickOpenWidget.getElement().style.top = `${titlebarOffset}px`; } - - if (this.pickOpenWidget) { - this.pickOpenWidget.getElement().style.top = `${titlebarOffset}px`; - } } - private handleOnShow(isPicker: boolean): void { - if (isPicker && this.quickOpenWidget) { - this.quickOpenWidget.hide(HideReason.FOCUS_LOST); - } else if (!isPicker && this.pickOpenWidget) { - this.pickOpenWidget.hide(HideReason.FOCUS_LOST); - } - + private handleOnShow(): void { this.emitQuickOpenVisibilityChange(true); } - private handleOnHide(isPicker: boolean, reason: HideReason): void { - if (!isPicker) { + private handleOnHide(reason: HideReason): void { - // Clear state - this.previousActiveHandlerDescriptor = null; + // Clear state + this.previousActiveHandlerDescriptor = null; - // Pass to handlers - for (let prefix in this.mapResolvedHandlersToPrefix) { - if (this.mapResolvedHandlersToPrefix.hasOwnProperty(prefix)) { - const promise = this.mapResolvedHandlersToPrefix[prefix]; - promise.then(handler => { - this.handlerOnOpenCalled[prefix] = false; + // Cancel pending results calls + this.cancelPendingGetResultsInvocation(); - handler.onClose(reason === HideReason.CANCELED); // Don't check if onOpen was called to preserve old behaviour for now - }); - } - } + // Pass to handlers + for (let prefix in this.mapResolvedHandlersToPrefix) { + const promise = this.mapResolvedHandlersToPrefix[prefix]; + promise.then(handler => { + this.handlerOnOpenCalled[prefix] = false; - // Complete promises that are waiting - while (this.promisesToCompleteOnHide.length) { - this.promisesToCompleteOnHide.pop()(true); - } + handler.onClose(reason === HideReason.CANCELED); // Don't check if onOpen was called to preserve old behaviour for now + }); + } + + // Complete promises that are waiting + while (this.promisesToCompleteOnHide.length) { + this.promisesToCompleteOnHide.pop()(true); } if (reason !== HideReason.FOCUS_LOST) { @@ -585,6 +272,14 @@ export class QuickOpenController extends Component implements IQuickOpenService this.emitQuickOpenVisibilityChange(false); } + private cancelPendingGetResultsInvocation(): void { + if (this.pendingGetResultsInvocation) { + this.pendingGetResultsInvocation.cancel(); + this.pendingGetResultsInvocation.dispose(); + this.pendingGetResultsInvocation = null; + } + } + private resetQuickOpenContextKeys(): void { Object.keys(this.mapContextKeyToContext).forEach(k => this.mapContextKeyToContext[k].reset()); } @@ -627,6 +322,12 @@ export class QuickOpenController extends Component implements IQuickOpenService private onType(value: string): void { + // cancel any pending get results invocation and create new + this.cancelPendingGetResultsInvocation(); + const pendingResultsInvocationTokenSource = new CancellationTokenSource(); + const pendingResultsInvocationToken = pendingResultsInvocationTokenSource.token; + this.pendingGetResultsInvocation = pendingResultsInvocationTokenSource; + // look for a handler const registry = Registry.as(Extensions.Quickopen); const handlerDescriptor = registry.getQuickOpenHandler(value); @@ -634,10 +335,6 @@ export class QuickOpenController extends Component implements IQuickOpenService const instantProgress = handlerDescriptor && handlerDescriptor.instantProgress; const contextKey = handlerDescriptor ? handlerDescriptor.contextKey : defaultHandlerDescriptor.contextKey; - // Use a generated token to avoid race conditions from long running promises - const currentResultToken = defaultGenerator.nextId(); - this.currentResultToken = currentResultToken; - // Reset Progress if (!instantProgress) { this.quickOpenWidget.getProgressBar().stop().hide(); @@ -656,8 +353,7 @@ export class QuickOpenController extends Component implements IQuickOpenService if (!trimmedValue) { // Trigger onOpen - this.resolveHandler(handlerDescriptor || defaultHandlerDescriptor) - .done(null, errors.onUnexpectedError); + this.resolveHandler(handlerDescriptor || defaultHandlerDescriptor); this.quickOpenWidget.setInput(this.getEditorHistoryWithGroupLabel(), { autoFocusFirstEntry: true }); @@ -668,42 +364,47 @@ export class QuickOpenController extends Component implements IQuickOpenService let resultPromiseDone = false; if (handlerDescriptor) { - resultPromise = this.handleSpecificHandler(handlerDescriptor, value, currentResultToken); + resultPromise = this.handleSpecificHandler(handlerDescriptor, value, pendingResultsInvocationToken); } // Otherwise handle default handlers if no specific handler present else { - resultPromise = this.handleDefaultHandler(defaultHandlerDescriptor, value, currentResultToken); + resultPromise = this.handleDefaultHandler(defaultHandlerDescriptor, value, pendingResultsInvocationToken); } // Remember as the active one this.previousActiveHandlerDescriptor = handlerDescriptor; // Progress if task takes a long time - TPromise.timeout(instantProgress ? 0 : 800).then(() => { - if (!resultPromiseDone && currentResultToken === this.currentResultToken) { + setTimeout(() => { + if (!resultPromiseDone && !pendingResultsInvocationToken.isCancellationRequested) { this.quickOpenWidget.getProgressBar().infinite().show(); } - }); + }, instantProgress ? 0 : 800); // Promise done handling - resultPromise.done(() => { + resultPromise.then(() => { resultPromiseDone = true; - if (currentResultToken === this.currentResultToken) { + if (!pendingResultsInvocationToken.isCancellationRequested) { this.quickOpenWidget.getProgressBar().hide(); } + + pendingResultsInvocationTokenSource.dispose(); }, (error: any) => { resultPromiseDone = true; + + pendingResultsInvocationTokenSource.dispose(); + errors.onUnexpectedError(error); this.notificationService.error(types.isString(error) ? new Error(error) : error); }); } - private handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, currentResultToken: string): TPromise { + private handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): TPromise { // Fill in history results if matching - const matchingHistoryEntries = this.editorHistoryHandler.getResults(value); + const matchingHistoryEntries = this.editorHistoryHandler.getResults(value, token); if (matchingHistoryEntries.length > 0) { matchingHistoryEntries[0] = new EditorHistoryEntryGroup(matchingHistoryEntries[0], nls.localize('historyMatches', "recently opened"), false); } @@ -719,8 +420,15 @@ export class QuickOpenController extends Component implements IQuickOpenService const previousInput = this.quickOpenWidget.getInput(); const wasShowingHistory = previousInput && previousInput.entries && previousInput.entries.some(e => e instanceof EditorHistoryEntry || e instanceof EditorHistoryEntryGroup); if (wasShowingHistory || matchingHistoryEntries.length > 0) { - (resolvedHandler.hasShortResponseTime() ? TPromise.timeout(QuickOpenController.MAX_SHORT_RESPONSE_TIME) : TPromise.as(undefined)).then(() => { - if (this.currentResultToken === currentResultToken && !inputSet) { + let responseDelay: Thenable; + if (resolvedHandler.hasShortResponseTime()) { + responseDelay = timeout(QuickOpenController.MAX_SHORT_RESPONSE_TIME); + } else { + responseDelay = Promise.resolve(void 0); + } + + responseDelay.then(() => { + if (!token.isCancellationRequested && !inputSet) { this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true }); inputSet = true; } @@ -728,8 +436,8 @@ export class QuickOpenController extends Component implements IQuickOpenService } // Get results - return resolvedHandler.getResults(value).then(result => { - if (this.currentResultToken === currentResultToken) { + return resolvedHandler.getResults(value, token).then(result => { + if (!token.isCancellationRequested) { // now is the time to show the input if we did not have set it before if (!inputSet) { @@ -775,7 +483,7 @@ export class QuickOpenController extends Component implements IQuickOpenService } } - private handleSpecificHandler(handlerDescriptor: QuickOpenHandlerDescriptor, value: string, currentResultToken: string): TPromise { + private handleSpecificHandler(handlerDescriptor: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): TPromise { return this.resolveHandler(handlerDescriptor).then((resolvedHandler: QuickOpenHandler) => { // Remove handler prefix from search value @@ -804,8 +512,8 @@ export class QuickOpenController extends Component implements IQuickOpenService } // Receive Results from Handler and apply - return resolvedHandler.getResults(value).then(result => { - if (this.currentResultToken === currentResultToken) { + return resolvedHandler.getResults(value, token).then(result => { + if (!token.isCancellationRequested) { if (!result || !result.entries.length) { const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(resolvedHandler.getEmptyLabel(value))]); this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), resolvedHandler.getAriaLabel()); @@ -880,27 +588,11 @@ export class QuickOpenController extends Component implements IQuickOpenService return this.mapResolvedHandlersToPrefix[id] = TPromise.as(handler.instantiate(this.instantiationService)); } - public layout(dimension: Dimension): void { - this.layoutDimensions = dimension; + layout(dimension: Dimension): void { + this.dimension = dimension; if (this.quickOpenWidget) { - this.quickOpenWidget.layout(this.layoutDimensions); + this.quickOpenWidget.layout(this.dimension); } - - if (this.pickOpenWidget) { - this.pickOpenWidget.layout(this.layoutDimensions); - } - } - - public dispose(): void { - if (this.quickOpenWidget) { - this.quickOpenWidget.dispose(); - } - - if (this.pickOpenWidget) { - this.pickOpenWidget.dispose(); - } - - super.dispose(); } } @@ -913,174 +605,11 @@ class PlaceholderQuickOpenEntry extends QuickOpenEntryGroup { this.placeHolderLabel = placeHolderLabel; } - public getLabel(): string { + getLabel(): string { return this.placeHolderLabel; } } -class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem { - private _shouldRunWithContext: IEntryRunContext; - private description: string; - private detail: string; - private tooltip: string; - private descriptionTooltip: string; - private hasSeparator: boolean; - private separatorLabel: string; - private alwaysShow: boolean; - private resource: URI; - private fileKind: FileKind; - private _action: IAction; - private removed: boolean; - private payload: any; - private labelOcticons: IParsedOcticons; - private descriptionOcticons: IParsedOcticons; - private detailOcticons: IParsedOcticons; - - constructor( - item: IPickOpenEntry, - private _index: number, - private onPreview: () => void, - private onRemove: () => void, - @IModeService private modeService: IModeService, - @IModelService private modelService: IModelService - ) { - super(item.label); - - this.description = item.description; - this.detail = item.detail; - this.tooltip = item.tooltip; - this.descriptionOcticons = item.description ? parseOcticons(item.description) : void 0; - this.descriptionTooltip = this.descriptionOcticons ? this.descriptionOcticons.text : void 0; - this.hasSeparator = item.separator && item.separator.border; - this.separatorLabel = item.separator && item.separator.label; - this.alwaysShow = item.alwaysShow; - this._action = item.action; - this.payload = item.payload; - - const fileItem = item; - this.resource = fileItem.resource; - this.fileKind = fileItem.fileKind; - } - - public matchesFuzzy(query: string, options: IInternalPickOptions): { labelHighlights: IMatch[], descriptionHighlights: IMatch[], detailHighlights: IMatch[] } { - if (!this.labelOcticons) { - this.labelOcticons = parseOcticons(this.getLabel()); // parse on demand - } - - const detail = this.getDetail(); - if (detail && options.matchOnDetail && !this.detailOcticons) { - this.detailOcticons = parseOcticons(detail); // parse on demand - } - - return { - labelHighlights: matchesFuzzyOcticonAware(query, this.labelOcticons), - descriptionHighlights: options.matchOnDescription && this.descriptionOcticons ? matchesFuzzyOcticonAware(query, this.descriptionOcticons) : void 0, - detailHighlights: options.matchOnDetail && this.detailOcticons ? matchesFuzzyOcticonAware(query, this.detailOcticons) : void 0 - }; - } - - public getPayload(): any { - return this.payload; - } - - public remove(): void { - super.setHidden(true); - this.removed = true; - - this.onRemove(); - } - - public isHidden(): boolean { - return this.removed || super.isHidden(); - } - - public get action(): IAction { - return this._action; - } - - public get index(): number { - return this._index; - } - - public getLabelOptions(): IIconLabelValueOptions { - return { - extraClasses: this.resource ? getIconClasses(this.modelService, this.modeService, this.resource, this.fileKind) : [] - }; - } - - public get shouldRunWithContext(): IEntryRunContext { - return this._shouldRunWithContext; - } - - public getDescription(): string { - return this.description; - } - - public getDetail(): string { - return this.detail; - } - - public getTooltip(): string { - return this.tooltip; - } - - public getDescriptionTooltip(): string { - return this.descriptionTooltip; - } - - public showBorder(): boolean { - return this.hasSeparator; - } - - public getGroupLabel(): string { - return this.separatorLabel; - } - - public shouldAlwaysShow(): boolean { - return this.alwaysShow; - } - - public getResource(): URI { - return this.resource; - } - - public run(mode: Mode, context: IEntryRunContext): boolean { - if (mode === Mode.OPEN) { - this._shouldRunWithContext = context; - - return true; - } - - if (mode === Mode.PREVIEW && this.onPreview) { - this.onPreview(); - } - - return false; - } -} - -class PickOpenActionProvider implements IActionProvider { - public hasActions(tree: ITree, element: PickOpenEntry): boolean { - return !!element.action; - } - - public getActions(tree: ITree, element: PickOpenEntry): TPromise { - return TPromise.as(element.action ? [element.action] : []); - } - - public hasSecondaryActions(tree: ITree, element: PickOpenEntry): boolean { - return false; - } - - public getSecondaryActions(tree: ITree, element: PickOpenEntry): TPromise { - return TPromise.as([]); - } - - public getActionItem(tree: ITree, element: PickOpenEntry, action: Action): BaseActionItem { - return null; - } -} - class EditorHistoryHandler { private scorerCache: ScorerCache; @@ -1092,7 +621,7 @@ class EditorHistoryHandler { this.scorerCache = Object.create(null); } - public getResults(searchValue?: string): QuickOpenEntry[] { + getResults(searchValue?: string, token?: CancellationToken): QuickOpenEntry[] { // Massage search for scoring const query = prepareQuery(searchValue); @@ -1137,7 +666,7 @@ class EditorHistoryHandler { // Sort by score and provide a fallback sorter that keeps the // recency of items in case the score for items is the same - .sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache, (e1, e2, query, accessor) => -1)); + .sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache, () => -1)); } } @@ -1147,7 +676,7 @@ class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass { super(); } - public getItemDescription(entry: QuickOpenEntry): string { + getItemDescription(entry: QuickOpenEntry): string { return this.allowMatchOnDescription ? entry.getDescription() : void 0; } } @@ -1172,9 +701,8 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, @ITextFileService private textFileService: ITextFileService, - @IWorkspaceContextService contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService, - @IEnvironmentService environmentService: IEnvironmentService, + @ILabelService labelService: ILabelService, @IFileService fileService: IFileService ) { super(editorService); @@ -1189,8 +717,8 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { } else { const resourceInput = input as IResourceInput; this.resource = resourceInput.resource; - this.label = labels.getBaseLabel(resourceInput.resource); - this.description = labels.getPathLabel(resources.dirname(this.resource), contextService, environmentService); + this.label = resources.basenameOrAuthority(resourceInput.resource); + this.description = labelService.getUriLabel(resources.dirname(this.resource), true); this.dirty = this.resource && this.textFileService.isDirty(this.resource); if (this.dirty && this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { @@ -1199,37 +727,37 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { } } - public getIcon(): string { + getIcon(): string { return this.dirty ? 'dirty' : ''; } - public getLabel(): string { + getLabel(): string { return this.label; } - public getLabelOptions(): IIconLabelValueOptions { + getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.resource) }; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, recently opened", this.getLabel()); } - public getDescription(): string { + getDescription(): string { return this.description; } - public getResource(): URI { + getResource(): URI { return this.resource; } - public getInput(): IEditorInput | IResourceInput { + getInput(): IEditorInput | IResourceInput { return this.input; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { const sideBySide = !context.quickNavigateConfiguration && (context.keymods.alt || context.keymods.ctrlCmd); const pinned = !this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen || context.keymods.alt; @@ -1261,21 +789,23 @@ function resourceForEditorHistory(input: EditorInput, fileService: IFileService) export class RemoveFromEditorHistoryAction extends Action { - public static readonly ID = 'workbench.action.removeFromEditorHistory'; - public static readonly LABEL = nls.localize('removeFromEditorHistory', "Remove From History"); + static readonly ID = 'workbench.action.removeFromEditorHistory'; + static readonly LABEL = nls.localize('removeFromEditorHistory', "Remove From History"); constructor( id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, + @IModelService private modelService: IModelService, + @IModeService private modeService: IModeService, @IInstantiationService private instantiationService: IInstantiationService, @IHistoryService private historyService: IHistoryService ) { super(id, label); } - public run(): TPromise { - interface IHistoryPickEntry extends IFilePickOpenEntry { + run(): TPromise { + interface IHistoryPickEntry extends IQuickPickItem { input: IEditorInput | IResourceInput; } @@ -1285,13 +815,13 @@ export class RemoveFromEditorHistoryAction extends Action { return { input: h, - resource: entry.getResource(), + iconClasses: getIconClasses(this.modelService, this.modeService, entry.getResource()), label: entry.getLabel(), description: entry.getDescription() }; }); - return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickHistory', "Select an editor entry to remove from history"), autoFocus: { autoFocusFirstEntry: true }, matchOnDescription: true }).then(pick => { + return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickHistory', "Select an editor entry to remove from history"), matchOnDescription: true }).then(pick => { if (pick) { this.historyService.remove(pick.input); } diff --git a/src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts b/src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts index 3f4ad79d9b0..95a8a593377 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickopen.contribution.ts @@ -9,14 +9,14 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { RemoveFromEditorHistoryAction } from 'vs/workbench/browser/parts/quickopen/quickOpenController'; import { QuickOpenSelectNextAction, QuickOpenSelectPreviousAction, inQuickOpenContext, getQuickNavigateHandler, QuickOpenNavigateNextAction, QuickOpenNavigatePreviousAction, defaultQuickOpenContext, QUICKOPEN_ACTION_ID, QUICKOPEN_ACION_LABEL } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.closeQuickOpen', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: inQuickOpenContext, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], handler: accessor => { @@ -29,7 +29,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.acceptSelectedQuickOpenItem', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: inQuickOpenContext, primary: null, handler: accessor => { @@ -42,7 +42,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.focusQuickOpen', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: inQuickOpenContext, primary: null, handler: accessor => { @@ -59,7 +59,7 @@ const globalQuickOpenKeybinding = { primary: KeyMod.CtrlCmd | KeyCode.KEY_P, sec KeybindingsRegistry.registerKeybindingRule({ id: QUICKOPEN_ACTION_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: globalQuickOpenKeybinding.primary, secondary: globalQuickOpenKeybinding.secondary, @@ -70,8 +70,8 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: QUICKOPEN_ACTION_ID, title: QUICKOPEN_ACION_LABEL } }); -registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectNextAction, QuickOpenSelectNextAction.ID, QuickOpenSelectNextAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_N } }, inQuickOpenContext, KeybindingsRegistry.WEIGHT.workbenchContrib(50)), 'Select Next in Quick Open'); -registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectPreviousAction, QuickOpenSelectPreviousAction.ID, QuickOpenSelectPreviousAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_P } }, inQuickOpenContext, KeybindingsRegistry.WEIGHT.workbenchContrib(50)), 'Select Previous in Quick Open'); +registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectNextAction, QuickOpenSelectNextAction.ID, QuickOpenSelectNextAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_N } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Next in Quick Open'); +registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenSelectPreviousAction, QuickOpenSelectPreviousAction.ID, QuickOpenSelectPreviousAction.LABEL, { primary: null, mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_P } }, inQuickOpenContext, KeybindingWeight.WorkbenchContrib + 50), 'Select Previous in Quick Open'); registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenNavigateNextAction, QuickOpenNavigateNextAction.ID, QuickOpenNavigateNextAction.LABEL), 'Navigate Next in Quick Open'); registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenNavigatePreviousAction, QuickOpenNavigatePreviousAction.ID, QuickOpenNavigatePreviousAction.LABEL), 'Navigate Previous in Quick Open'); registry.registerWorkbenchAction(new SyncActionDescriptor(RemoveFromEditorHistoryAction, RemoveFromEditorHistoryAction.ID, RemoveFromEditorHistoryAction.LABEL), 'Remove From History'); @@ -79,7 +79,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(RemoveFromEditorHistor const quickOpenNavigateNextInFilePickerId = 'workbench.action.quickOpenNavigateNextInFilePicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigateNextInFilePickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigateNextInFilePickerId, true), when: defaultQuickOpenContext, primary: globalQuickOpenKeybinding.primary, @@ -90,7 +90,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const quickOpenNavigatePreviousInFilePickerId = 'workbench.action.quickOpenNavigatePreviousInFilePicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigatePreviousInFilePickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigatePreviousInFilePickerId, false), when: defaultQuickOpenContext, primary: globalQuickOpenKeybinding.primary | KeyMod.Shift, diff --git a/src/vs/workbench/browser/parts/quickopen/quickopen.ts b/src/vs/workbench/browser/parts/quickopen/quickopen.ts index a13daae1be5..f0a4175911d 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickopen.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickopen.ts @@ -53,7 +53,7 @@ export class BaseQuickOpenNavigateAction extends Action { super(id, label); } - public run(event?: any): TPromise { + run(event?: any): TPromise { const keys = this.keybindingService.lookupKeybindings(this.id); const quickNavigate = this.quickNavigate ? { keybindings: keys } : void 0; @@ -80,8 +80,8 @@ export function getQuickNavigateHandler(id: string, next?: boolean): ICommandHan export class QuickOpenNavigateNextAction extends BaseQuickOpenNavigateAction { - public static readonly ID = 'workbench.action.quickOpenNavigateNext'; - public static readonly LABEL = nls.localize('quickNavigateNext', "Navigate Next in Quick Open"); + static readonly ID = 'workbench.action.quickOpenNavigateNext'; + static readonly LABEL = nls.localize('quickNavigateNext', "Navigate Next in Quick Open"); constructor( id: string, @@ -96,8 +96,8 @@ export class QuickOpenNavigateNextAction extends BaseQuickOpenNavigateAction { export class QuickOpenNavigatePreviousAction extends BaseQuickOpenNavigateAction { - public static readonly ID = 'workbench.action.quickOpenNavigatePrevious'; - public static readonly LABEL = nls.localize('quickNavigatePrevious', "Navigate Previous in Quick Open"); + static readonly ID = 'workbench.action.quickOpenNavigatePrevious'; + static readonly LABEL = nls.localize('quickNavigatePrevious', "Navigate Previous in Quick Open"); constructor( id: string, @@ -112,8 +112,8 @@ export class QuickOpenNavigatePreviousAction extends BaseQuickOpenNavigateAction export class QuickOpenSelectNextAction extends BaseQuickOpenNavigateAction { - public static readonly ID = 'workbench.action.quickOpenSelectNext'; - public static readonly LABEL = nls.localize('quickSelectNext', "Select Next in Quick Open"); + static readonly ID = 'workbench.action.quickOpenSelectNext'; + static readonly LABEL = nls.localize('quickSelectNext', "Select Next in Quick Open"); constructor( id: string, @@ -128,8 +128,8 @@ export class QuickOpenSelectNextAction extends BaseQuickOpenNavigateAction { export class QuickOpenSelectPreviousAction extends BaseQuickOpenNavigateAction { - public static readonly ID = 'workbench.action.quickOpenSelectPrevious'; - public static readonly LABEL = nls.localize('quickSelectPrevious', "Select Previous in Quick Open"); + static readonly ID = 'workbench.action.quickOpenSelectPrevious'; + static readonly LABEL = nls.localize('quickSelectPrevious', "Select Previous in Quick Open"); constructor( id: string, diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 312dae4c272..9a5f5bd6e00 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -12,7 +12,7 @@ visibility: hidden !important; } -.monaco-workbench > .sidebar > .title > .title-label span { +.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 d4826a2b8f6..556cdfd222e 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -26,15 +26,12 @@ 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 } from 'vs/base/browser/dom'; -import { $ } from 'vs/base/browser/builder'; +import { Dimension, EventType, addDisposableListener } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export class SidebarPart extends CompositePart { - public static readonly activeViewletSettingsKey = 'workbench.sidebar.activeviewletid'; - - public _serviceBrand: any; + static readonly activeViewletSettingsKey = 'workbench.sidebar.activeviewletid'; private blockOpeningViewlet: boolean; @@ -69,41 +66,44 @@ export class SidebarPart extends CompositePart { ); } - public get onDidViewletOpen(): Event { + get onDidViewletOpen(): Event { return this._onDidCompositeOpen.event as Event; } - public get onDidViewletClose(): Event { + get onDidViewletClose(): Event { return this._onDidCompositeClose.event as Event; } - public createTitleArea(parent: HTMLElement): HTMLElement { + createTitleArea(parent: HTMLElement): HTMLElement { const titleArea = super.createTitleArea(parent); - $(titleArea).on(EventType.CONTEXT_MENU, (e: MouseEvent) => this.onTitleAreaContextMenu(new StandardMouseEvent(e))); + + this._register(addDisposableListener(titleArea, EventType.CONTEXT_MENU, e => { + this.onTitleAreaContextMenu(new StandardMouseEvent(e)); + })); return titleArea; } - public updateStyles(): void { + updateStyles(): void { super.updateStyles(); // Part container - const container = $(this.getContainer()); + const container = this.getContainer(); - container.style('background-color', this.getColor(SIDE_BAR_BACKGROUND)); - container.style('color', this.getColor(SIDE_BAR_FOREGROUND)); + container.style.backgroundColor = this.getColor(SIDE_BAR_BACKGROUND); + container.style.color = this.getColor(SIDE_BAR_FOREGROUND); const borderColor = this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder); const isPositionLeft = this.partService.getSideBarPosition() === SideBarPosition.LEFT; - container.style('border-right-width', borderColor && isPositionLeft ? '1px' : null); - container.style('border-right-style', borderColor && isPositionLeft ? 'solid' : null); - container.style('border-right-color', isPositionLeft ? borderColor : null); - container.style('border-left-width', borderColor && !isPositionLeft ? '1px' : null); - container.style('border-left-style', borderColor && !isPositionLeft ? 'solid' : null); - container.style('border-left-color', !isPositionLeft ? borderColor : null); + container.style.borderRightWidth = borderColor && isPositionLeft ? '1px' : null; + container.style.borderRightStyle = borderColor && isPositionLeft ? 'solid' : null; + container.style.borderRightColor = isPositionLeft ? borderColor : null; + container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : null; + container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : null; + container.style.borderLeftColor = !isPositionLeft ? borderColor : null; } - public openViewlet(id: string, focus?: boolean): TPromise { + openViewlet(id: string, focus?: boolean): TPromise { if (this.blockOpeningViewlet) { return TPromise.as(null); // Workaround against a potential race condition } @@ -122,19 +122,19 @@ export class SidebarPart extends CompositePart { return promise.then(() => this.openComposite(id, focus)) as TPromise; } - public getActiveViewlet(): IViewlet { + getActiveViewlet(): IViewlet { return this.getActiveComposite(); } - public getLastActiveViewletId(): string { + getLastActiveViewletId(): string { return this.getLastActiveCompositetId(); } - public hideActiveViewlet(): TPromise { + hideActiveViewlet(): TPromise { return this.hideActiveComposite().then(composite => void 0); } - public layout(dimension: Dimension): Dimension[] { + layout(dimension: Dimension): Dimension[] { if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { return [dimension]; } @@ -162,8 +162,8 @@ export class SidebarPart extends CompositePart { class FocusSideBarAction extends Action { - public static readonly ID = 'workbench.action.focusSideBar'; - public static readonly LABEL = nls.localize('focusSideBar', "Focus into Side Bar"); + static readonly ID = 'workbench.action.focusSideBar'; + static readonly LABEL = nls.localize('focusSideBar', "Focus into Side Bar"); constructor( id: string, @@ -174,7 +174,7 @@ class FocusSideBarAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { // Show side bar if (!this.partService.isVisible(Parts.SIDEBAR_PART)) { diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index c0f06e8e485..ca12bec91c5 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -5,6 +5,7 @@ .monaco-workbench > .part.statusbar { box-sizing: border-box; + cursor: default; width: 100%; height: 22px; font-size: 12px; @@ -52,7 +53,6 @@ } .monaco-workbench > .part.statusbar > .statusbar-entry > span { - cursor: default; height: 100%; } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbar.ts b/src/vs/workbench/browser/parts/statusbar/statusbar.ts index d46fd03dc31..f51d5a858aa 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbar.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbar.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IDisposable } from 'vs/base/common/lifecycle'; -import * as statusbarService from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; @@ -14,13 +14,10 @@ export interface IStatusbarItem { render(element: HTMLElement): IDisposable; } -export import StatusbarAlignment = statusbarService.StatusbarAlignment; - export class StatusbarItemDescriptor { - - public syncDescriptor: SyncDescriptor0; - public alignment: StatusbarAlignment; - public priority: number; + syncDescriptor: SyncDescriptor0; + alignment: StatusbarAlignment; + priority: number; constructor(ctor: IConstructorSignature0, alignment?: StatusbarAlignment, priority?: number) { this.syncDescriptor = createSyncDescriptor(ctor); @@ -42,11 +39,11 @@ class StatusbarRegistry implements IStatusbarRegistry { this._items = []; } - public get items(): StatusbarItemDescriptor[] { + get items(): StatusbarItemDescriptor[] { return this._items; } - public registerStatusbarItem(descriptor: StatusbarItemDescriptor): void { + registerStatusbarItem(descriptor: StatusbarItemDescriptor): void { this._items.push(descriptor); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index beebc378ade..25b7534e351 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -9,17 +9,16 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { TPromise } from 'vs/base/common/winjs.base'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { $ } from 'vs/base/browser/builder'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Part } from 'vs/workbench/browser/part'; -import { StatusbarAlignment, IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -28,15 +27,15 @@ 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 } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; export class StatusbarPart extends Part implements IStatusbarService { - public _serviceBrand: any; + _serviceBrand: any; - private static readonly PRIORITY_PROP = 'priority'; - private static readonly ALIGNMENT_PROP = 'alignment'; + private static readonly PRIORITY_PROP = 'statusbar-entry-priority'; + private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment'; private statusItemsContainer: HTMLElement; private statusMsgDispose: IDisposable; @@ -55,10 +54,10 @@ export class StatusbarPart extends Part implements IStatusbarService { } private registerListeners(): void { - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - public addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IDisposable { + addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IDisposable { // Render entry in status bar const el = this.doCreateStatusItem(alignment, priority, entry.showBeak ? 'has-beak' : void 0); @@ -71,7 +70,7 @@ export class StatusbarPart extends Part implements IStatusbarService { let inserted = false; for (let i = 0; i < neighbours.length; i++) { const neighbour = neighbours[i]; - const nPriority = $(neighbour).getProperty(StatusbarPart.PRIORITY_PROP); + const nPriority = Number(neighbour.getAttribute(StatusbarPart.PRIORITY_PROP)); if ( alignment === StatusbarAlignment.LEFT && nPriority < priority || alignment === StatusbarAlignment.RIGHT && nPriority > priority @@ -86,15 +85,13 @@ export class StatusbarPart extends Part implements IStatusbarService { container.appendChild(el); } - return { - dispose: () => { - $(el).destroy(); + return toDisposable(() => { + el.remove(); - if (toDispose) { - toDispose.dispose(); - } + if (toDispose) { + toDispose.dispose(); } - }; + }); } private getEntries(alignment: StatusbarAlignment): HTMLElement[] { @@ -104,7 +101,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const children = container.children; for (let i = 0; i < children.length; i++) { const childElement = children.item(i); - if ($(childElement).getProperty(StatusbarPart.ALIGNMENT_PROP) === alignment) { + if (Number(childElement.getAttribute(StatusbarPart.ALIGNMENT_PROP)) === alignment) { entries.push(childElement); } } @@ -112,7 +109,7 @@ export class StatusbarPart extends Part implements IStatusbarService { return entries; } - public createContentArea(parent: HTMLElement): HTMLElement { + createContentArea(parent: HTMLElement): HTMLElement { this.statusItemsContainer = parent; // Fill in initial items that were contributed from the registry @@ -122,16 +119,13 @@ export class StatusbarPart extends Part implements IStatusbarService { const rightDescriptors = registry.items.filter(d => d.alignment === StatusbarAlignment.RIGHT).sort((a, b) => a.priority - b.priority); const descriptors = rightDescriptors.concat(leftDescriptors); // right first because they float - - this.toUnbind.push(...descriptors.map(descriptor => { + descriptors.forEach(descriptor => { const item = this.instantiationService.createInstance(descriptor.syncDescriptor); const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority); - const dispose = item.render(el); + this._register(item.render(el)); this.statusItemsContainer.appendChild(el); - - return dispose; - })); + }); return this.statusItemsContainer; } @@ -139,22 +133,22 @@ export class StatusbarPart extends Part implements IStatusbarService { protected updateStyles(): void { super.updateStyles(); - const container = $(this.getContainer()); + const container = this.getContainer(); // Background colors const backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND); - container.style('background-color', backgroundColor); - container.style('color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); + container.style.backgroundColor = backgroundColor; + container.style.color = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND); // Border color const borderColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder); - container.style('border-top-width', borderColor ? '1px' : null); - container.style('border-top-style', borderColor ? 'solid' : null); - container.style('border-top-color', borderColor); + container.style.borderTopWidth = borderColor ? '1px' : null; + container.style.borderTopStyle = borderColor ? 'solid' : null; + container.style.borderTopColor = borderColor; // Notification Beak if (!this.styleElement) { - this.styleElement = createStyleSheet(container.getHTMLElement()); + this.styleElement = createStyleSheet(container); } this.styleElement.innerHTML = `.monaco-workbench > .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; @@ -173,13 +167,13 @@ export class StatusbarPart extends Part implements IStatusbarService { addClass(el, 'left'); } - $(el).setProperty(StatusbarPart.PRIORITY_PROP, priority); - $(el).setProperty(StatusbarPart.ALIGNMENT_PROP, alignment); + el.setAttribute(StatusbarPart.PRIORITY_PROP, String(priority)); + el.setAttribute(StatusbarPart.ALIGNMENT_PROP, String(alignment)); return el; } - public setStatusMessage(message: string, autoDisposeAfter: number = -1, delayBy: number = 0): IDisposable { + setStatusMessage(message: string, autoDisposeAfter: number = -1, delayBy: number = 0): IDisposable { if (this.statusMsgDispose) { this.statusMsgDispose.dispose(); // dismiss any previous } @@ -238,7 +232,7 @@ class StatusBarEntryItem implements IStatusbarItem { } } - public render(el: HTMLElement): IDisposable { + render(el: HTMLElement): IDisposable { let toDispose: IDisposable[] = []; addClass(el, 'statusbar-entry'); @@ -247,7 +241,7 @@ class StatusBarEntryItem implements IStatusbarItem { if (this.entry.command) { textContainer = document.createElement('a'); - $(textContainer).on('click', () => this.executeCommand(this.entry.command, this.entry.arguments), toDispose); + toDispose.push(addDisposableListener(textContainer, 'click', () => this.executeCommand(this.entry.command, this.entry.arguments))); } else { textContainer = document.createElement('span'); } @@ -257,7 +251,7 @@ class StatusBarEntryItem implements IStatusbarItem { // Tooltip if (this.entry.tooltip) { - $(textContainer).title(this.entry.tooltip); + textContainer.title = this.entry.tooltip; } // Color @@ -268,15 +262,15 @@ class StatusBarEntryItem implements IStatusbarItem { color = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString(); toDispose.push(this.themeService.onThemeChange(theme => { let colorValue = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString(); - $(textContainer).color(colorValue); + textContainer.style.color = colorValue; })); } - $(textContainer).color(color); + textContainer.style.color = color; } // Context Menu if (this.entry.extensionId) { - $(textContainer).on('contextmenu', e => { + toDispose.push(addDisposableListener(textContainer, 'contextmenu', e => { EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ @@ -284,7 +278,7 @@ class StatusBarEntryItem implements IStatusbarItem { getActionsContext: () => this.entry.extensionId, getActions: () => TPromise.as([manageExtensionAction]) }); - }, toDispose); + })); } el.appendChild(textContainer); @@ -312,7 +306,7 @@ class StatusBarEntryItem implements IStatusbarItem { } */ this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' }); - this.commandService.executeCommand(id, ...args).done(undefined, err => this.notificationService.error(toErrorMessage(err))); + this.commandService.executeCommand(id, ...args).then(undefined, err => this.notificationService.error(toErrorMessage(err))); } } @@ -324,7 +318,7 @@ class ManageExtensionAction extends Action { super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension")); } - public run(extensionId: string): TPromise { + run(extensionId: string): TPromise { return this.commandService.executeCommand('_extensions.manage', extensionId); } } diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg new file mode 100644 index 00000000000..bb243036bb5 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg new file mode 100644 index 00000000000..7abec27cd97 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg new file mode 100644 index 00000000000..b6645e8c829 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg new file mode 100644 index 00000000000..781322be05f --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg new file mode 100644 index 00000000000..1f6a7016f85 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg new file mode 100644 index 00000000000..80ecf45c9ab --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg new file mode 100644 index 00000000000..d9f814370b0 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg new file mode 100644 index 00000000000..3ab78151c17 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/code-icon.svg b/src/vs/workbench/browser/parts/titlebar/media/code-icon.svg new file mode 100644 index 00000000000..cc61f81ea5a --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/code-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 12c110e78fe..42965aae65c 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -6,26 +6,196 @@ .monaco-workbench > .part.titlebar { box-sizing: border-box; width: 100%; - font-size: 12px; padding: 0 70px; overflow: hidden; flex-shrink: 0; align-items: center; justify-content: center; user-select: none; - -webkit-app-region: drag; zoom: 1; /* prevent zooming */ line-height: 22px; height: 22px; display: flex; } +.monaco-workbench > .part.titlebar > .titlebar-drag-region { + top: 0; + left: 0; + display: block; + position: absolute; + width: 100%; + height: 100%; + z-index: -1; + -webkit-app-region: drag; +} + .monaco-workbench > .part.titlebar > .window-title { flex: 0 1 auto; + font-size: 12px; overflow: hidden; white-space: nowrap; - line-height: 22px; text-overflow: ellipsis; - -webkit-app-region: drag; + margin-left: auto; + margin-right: auto; zoom: 1; /* prevent zooming */ +} + +/* Windows/Linux: Rules for custom title (icon, window controls) */ + +.monaco-workbench.windows > .part.titlebar, +.monaco-workbench.linux > .part.titlebar { + padding: 0; + height: 30px; + line-height: 30px; + justify-content: left; + overflow: visible; +} + +.monaco-workbench.linux > .part.titlebar > .window-title { + font-size: inherit; +} + +.monaco-workbench.windows > .part.titlebar > .resizer, +.monaco-workbench.linux > .part.titlebar > .resizer { + -webkit-app-region: no-drag; + position: absolute; + top: 0; + width: 100%; + height: 20%; +} + +.monaco-workbench.windows.fullscreen > .part.titlebar > .resizer, +.monaco-workbench.linux.fullscreen > .part.titlebar > .resizer { + display: none; +} + + +.monaco-workbench > .part.titlebar > .window-appicon { + width: 35px; + height: 100%; + position: relative; + z-index: 99; + background-image: url('code-icon.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: 16px; + flex-shrink: 0; +} + +.monaco-workbench.fullscreen > .part.titlebar > .window-appicon { + display: none; +} + +.monaco-workbench > .part.titlebar > .window-controls-container { + display: flex; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + position: relative; + z-index: 99; + -webkit-app-region: no-drag; + height: 100%; + width: 138px; +} + +.monaco-workbench.fullscreen > .part.titlebar > .window-controls-container { + display: none; +} + +.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 { + shape-rendering: crispEdges; + text-align: center; +} + +.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 { + -webkit-mask: url('chrome-restore.svg') no-repeat 50% 50%; +} + +.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 { + -webkit-mask: url('chrome-minimize.svg') no-repeat 50% 50%; +} + +.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 { + background-color: rgba(255, 255, 255, 0.1); +} + +.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 { + background-color: rgba(232, 17, 35, 0.9); +} + +.monaco-workbench > .part.titlebar > .window-controls-container .window-icon.window-close:hover { + background-color: white; +} + +/* Menubar styles */ + +.monaco-workbench .menubar { + display: flex; + flex-shrink: 1; + box-sizing: border-box; + height: 30px; + -webkit-app-region: no-drag; + overflow: hidden; + flex-wrap: wrap; +} + +.monaco-workbench.fullscreen .menubar { + margin: 0px; + padding: 0px 5px; +} + +.monaco-workbench .menubar > .menubar-menu-button { + align-items: center; + box-sizing: border-box; + padding: 0px 8px; + cursor: default; + -webkit-app-region: no-drag; + zoom: 1; + white-space: nowrap; +} + +.monaco-workbench .menubar .menubar-menu-items-holder { + position: absolute; + left: 0px; + opacity: 1; + z-index: 2000; +} + +.monaco-workbench .menubar .menubar-menu-items-holder.monaco-menu-container { + font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback"; + outline: 0; + border: none; +} + +.monaco-workbench .menubar .menubar-menu-items-holder.monaco-menu-container :focus { + outline: 0; +} + +.hc-black .monaco-workbench .menubar .menubar-menu-items-holder.monaco-menu-container { + border: 2px solid #6FC3DF; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts new file mode 100644 index 00000000000..e43deecb44d --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -0,0 +1,1381 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vs/nls'; +import * as browser from 'vs/base/browser/browser'; +import * as strings from 'vs/base/common/strings'; +import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IWindowService, MenuBarVisibility, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ActionRunner, IActionRunner, IAction, Action } from 'vs/base/common/actions'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import * as DOM from 'vs/base/browser/dom'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { isMacintosh } from 'vs/base/common/platform'; +import { Menu, IMenuOptions, SubmenuAction, MENU_MNEMONIC_REGEX, cleanMnemonic, MENU_ESCAPED_MNEMONIC_REGEX } from 'vs/base/browser/ui/menu/menu'; +import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; +import { domEvent } from 'vs/base/browser/event'; +import { IRecentlyOpened } from 'vs/platform/history/common/history'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_BORDER, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, MENU_BACKGROUND, MENU_FOREGROUND, MENU_SELECTION_BACKGROUND, MENU_SELECTION_FOREGROUND, MENU_SELECTION_BORDER } from 'vs/workbench/common/theme'; +import { URI } from 'vs/base/common/uri'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import { Gesture, EventType, GestureEvent } from 'vs/base/browser/touch'; + +const $ = DOM.$; + +interface CustomMenu { + title: string; + buttonElement: HTMLElement; + titleElement: HTMLElement; + actions?: IAction[]; +} + +enum MenubarState { + HIDDEN, + VISIBLE, + FOCUSED, + OPEN +} + +export class MenubarControl extends Disposable { + + private keys = [ + 'files.autoSave', + 'window.menuBarVisibility', + 'editor.multiCursorModifier', + 'workbench.sideBar.location', + 'workbench.statusBar.visible', + 'workbench.activityBar.visible', + 'window.enableMenuBarMnemonics', + 'window.nativeTabs' + ]; + + private topLevelMenus: { + 'File': IMenu; + 'Edit': IMenu; + 'Selection': IMenu; + 'View': IMenu; + 'Go': IMenu; + 'Debug': IMenu; + 'Terminal': IMenu; + 'Window'?: IMenu; + 'Help': IMenu; + [index: string]: IMenu; + }; + + private topLevelTitles = { + 'File': nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), + 'Edit': nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"), + 'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), + 'View': nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), + 'Go': nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), + 'Debug': nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"), + 'Terminal': nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), + 'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") + }; + + private focusedMenu: { + index: number; + holder?: HTMLElement; + widget?: Menu; + }; + + private customMenus: CustomMenu[]; + + private menuUpdater: RunOnceScheduler; + private actionRunner: IActionRunner; + private focusToReturn: HTMLElement; + private container: HTMLElement; + private recentlyOpened: IRecentlyOpened; + private updatePending: boolean; + private _focusState: MenubarState; + + // Input-related + private _mnemonicsInUse: boolean; + private openedViaKeyboard: boolean; + private awaitingAltRelease: boolean; + private ignoreNextMouseUp: boolean; + private mnemonics: Map; + + private _onVisibilityChange: Emitter; + + private static MAX_MENU_RECENT_ENTRIES = 10; + + constructor( + @IThemeService themeService: IThemeService, + @IMenuService private menuService: IMenuService, + @IWindowService private windowService: IWindowService, + @IWindowsService private windowsService: IWindowsService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IKeybindingService private keybindingService: IKeybindingService, + @IConfigurationService private configurationService: IConfigurationService, + @ILabelService private labelService: ILabelService, + @IUpdateService private updateService: IUpdateService + ) { + + super(); + + this.topLevelMenus = { + 'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)), + 'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)), + 'Selection': this._register(this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService)), + 'View': this._register(this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService)), + 'Go': this._register(this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService)), + 'Debug': this._register(this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService)), + 'Terminal': this._register(this.menuService.createMenu(MenuId.MenubarTerminalMenu, this.contextKeyService)), + 'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService)) + }; + + if (isMacintosh) { + this.topLevelMenus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); + } + + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doSetupMenubar(), 200)); + + this.actionRunner = this._register(new ActionRunner()); + this._register(this.actionRunner.onDidBeforeRun(() => { + this.setUnfocusedState(); + })); + + this._onVisibilityChange = this._register(new Emitter()); + + if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') { + for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { + this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar())); + } + this.doSetupMenubar(); + } + + this._focusState = MenubarState.HIDDEN; + + this.windowService.getRecentlyOpened().then((recentlyOpened) => { + this.recentlyOpened = recentlyOpened; + }); + + this.registerListeners(); + } + + private get currentEnableMenuBarMnemonics(): boolean { + let enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics'); + if (typeof enableMenuBarMnemonics !== 'boolean') { + enableMenuBarMnemonics = true; + } + + return enableMenuBarMnemonics; + } + + private get currentAutoSaveSetting(): string { + return this.configurationService.getValue('files.autoSave'); + } + + private get currentSidebarPosition(): string { + return this.configurationService.getValue('workbench.sideBar.location'); + } + + private get currentStatusBarVisibility(): boolean { + let setting = this.configurationService.getValue('workbench.statusBar.visible'); + if (typeof setting !== 'boolean') { + setting = true; + } + + return setting; + } + + private get currentActivityBarVisibility(): boolean { + let setting = this.configurationService.getValue('workbench.activityBar.visible'); + if (typeof setting !== 'boolean') { + setting = true; + } + + return setting; + } + + private get currentMenubarVisibility(): MenuBarVisibility { + return this.configurationService.getValue('window.menuBarVisibility'); + } + + private get currentTitlebarStyleSetting(): string { + return this.configurationService.getValue('window.titleBarStyle'); + } + + private get focusState(): MenubarState { + return this._focusState; + } + + private set focusState(value: MenubarState) { + if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) { + // Losing focus, update the menu if needed + + if (this.updatePending) { + this.menuUpdater.schedule(); + this.updatePending = false; + } + } + + if (value === this._focusState) { + return; + } + + const isVisible = this.isVisible; + const isOpen = this.isOpen; + const isFocused = this.isFocused; + + this._focusState = value; + + switch (value) { + case MenubarState.HIDDEN: + if (isVisible) { + this.hideMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (isFocused) { + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.focus(); + this.focusToReturn = null; + } + } + + + break; + case MenubarState.VISIBLE: + if (!isVisible) { + this.showMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (isFocused) { + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.blur(); + } + + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.focus(); + this.focusToReturn = null; + } + } + + break; + case MenubarState.FOCUSED: + if (!isVisible) { + this.showMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (this.focusedMenu) { + this.customMenus[this.focusedMenu.index].buttonElement.focus(); + } + break; + case MenubarState.OPEN: + if (!isVisible) { + this.showMenubar(); + } + + if (this.focusedMenu) { + this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard); + } + break; + } + + this._focusState = value; + } + + private get mnemonicsInUse(): boolean { + return this._mnemonicsInUse; + } + + private set mnemonicsInUse(value: boolean) { + this._mnemonicsInUse = value; + } + + private get isVisible(): boolean { + return this.focusState >= MenubarState.VISIBLE; + } + + private get isFocused(): boolean { + return this.focusState >= MenubarState.FOCUSED; + } + + private get isOpen(): boolean { + return this.focusState >= MenubarState.OPEN; + } + + private onDidChangeFullscreen(): void { + this.setUnfocusedState(); + } + + private onDidChangeWindowFocus(hasFocus: boolean): void { + if (this.container) { + if (hasFocus) { + DOM.removeClass(this.container, 'inactive'); + } else { + DOM.addClass(this.container, 'inactive'); + this.setUnfocusedState(); + this.awaitingAltRelease = false; + } + } + } + + private onConfigurationUpdated(event: IConfigurationChangeEvent): void { + if (this.keys.some(key => event.affectsConfiguration(key))) { + this.setupMenubar(); + } + + if (event.affectsConfiguration('window.menuBarVisibility')) { + this.setUnfocusedState(); + } + } + + private setUnfocusedState(): void { + if (this.currentMenubarVisibility === 'toggle' || this.currentMenubarVisibility === 'hidden') { + this.focusState = MenubarState.HIDDEN; + } else if (this.currentMenubarVisibility === 'default' && browser.isFullscreen()) { + this.focusState = MenubarState.HIDDEN; + } else { + this.focusState = MenubarState.VISIBLE; + } + + this.ignoreNextMouseUp = false; + this.mnemonicsInUse = false; + this.updateMnemonicVisibility(false); + } + + private hideMenubar(): void { + this.container.style.display = 'none'; + this._onVisibilityChange.fire(false); + } + + private showMenubar(): void { + this.container.style.display = 'flex'; + this._onVisibilityChange.fire(true); + } + + private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { + const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + + if (this.currentMenubarVisibility === 'hidden') { + return; + } + + // Alt key pressed while menu is focused. This should return focus away from the menubar + if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) { + this.setUnfocusedState(); + this.mnemonicsInUse = false; + this.awaitingAltRelease = true; + } + + // Clean alt key press and release + if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') { + if (!this.awaitingAltRelease) { + if (!this.isFocused) { + this.mnemonicsInUse = true; + this.focusedMenu = { index: 0 }; + this.focusState = MenubarState.FOCUSED; + } else if (!this.isOpen) { + this.setUnfocusedState(); + } + } + } + + // Alt key released + if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') { + this.awaitingAltRelease = false; + } + + if (this.currentEnableMenuBarMnemonics && this.customMenus && !this.isOpen) { + this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse); + } + } + + private updateMnemonicVisibility(visible: boolean): void { + if (this.customMenus) { + this.customMenus.forEach(customMenu => { + if (customMenu.titleElement.children.length) { + let child = customMenu.titleElement.children.item(0) as HTMLElement; + if (child) { + child.style.textDecoration = visible ? 'underline' : null; + } + } + }); + } + } + + private onRecentlyOpenedChange(): void { + this.windowService.getRecentlyOpened().then(recentlyOpened => { + this.recentlyOpened = recentlyOpened; + this.setupMenubar(); + }); + } + + private registerListeners(): void { + // Update when config changes + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); + + // Listen to update service + this.updateService.onStateChange(() => this.setupMenubar()); + + // Listen for context changes + this._register(this.contextKeyService.onDidChangeContext(() => this.setupMenubar())); + + // Listen for changes in recently opened menu + this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); + + // Listen to keybindings change + this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar())); + + // These listeners only apply when the custom menubar is being used + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + // Listen to fullscreen changes + this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); + + // Listen for alt key presses + this._register(ModifierKeyEmitter.getInstance(this.windowService).event(this.onModifierKeyToggled, this)); + + // Listen for window focus changes + this._register(this.windowService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e))); + } + } + + private doSetupMenubar(): void { + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + this.setupCustomMenubar(); + } + + // TODO@sbatten Uncomment to bring back dynamic menubar + // else { + // // Send menus to main process to be rendered by Electron + // const menubarData = {}; + // if (this.getMenubarMenus(menubarData)) { + // this.menubarService.updateMenubar(this.windowService.getCurrentWindowId(), menubarData, this.getAdditionalKeybindings()); + // } + // } + } + + private setupMenubar(): void { + this.menuUpdater.schedule(); + } + + private registerMnemonic(menuIndex: number, mnemonic: string): void { + this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex); + } + + private setCheckedStatus(action: IAction | IMenubarMenuItemAction) { + switch (action.id) { + case 'workbench.action.toggleAutoSave': + action.checked = this.currentAutoSaveSetting !== 'off'; + break; + + default: + break; + } + } + + private calculateActionLabel(action: IAction | IMenubarMenuItemAction): string { + let label = action.label; + switch (action.id) { + case 'workbench.action.toggleSidebarPosition': + if (this.currentSidebarPosition !== 'right') { + label = nls.localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right"); + } else { + label = nls.localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left"); + } + break; + + case 'workbench.action.toggleStatusbarVisibility': + if (this.currentStatusBarVisibility) { + label = nls.localize({ key: 'miHideStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Hide Status Bar"); + } else { + label = nls.localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Show Status Bar"); + } + break; + + case 'workbench.action.toggleActivityBarVisibility': + if (this.currentActivityBarVisibility) { + label = nls.localize({ key: 'miHideActivityBar', comment: ['&& denotes a mnemonic'] }, "Hide &&Activity Bar"); + } else { + label = nls.localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"); + } + break; + + default: + break; + } + + return label; + } + + private createOpenRecentMenuAction(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, commandId: string, isFile: boolean): IAction { + + let label: string; + let uri: URI; + + if (isSingleFolderWorkspaceIdentifier(workspace) && !isFile) { + label = this.labelService.getWorkspaceLabel(workspace, { verbose: true }); + uri = workspace; + } else if (isWorkspaceIdentifier(workspace)) { + label = this.labelService.getWorkspaceLabel(workspace, { verbose: true }); + uri = URI.file(workspace.configPath); + } else { + uri = workspace; + label = this.labelService.getUriLabel(uri); + } + + return new Action(commandId, label, undefined, undefined, (event) => { + const openInNewWindow = event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); + + return this.windowService.openWindow([uri], { + forceNewWindow: openInNewWindow, + forceOpenWorkspaceAsFile: isFile + }); + }); + } + + private getOpenRecentActions(): IAction[] { + if (!this.recentlyOpened) { + return []; + } + + const { workspaces, files } = this.recentlyOpened; + + const result: IAction[] = []; + + if (workspaces.length > 0) { + for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { + result.push(this.createOpenRecentMenuAction(workspaces[i], 'openRecentWorkspace', false)); + } + + result.push(new Separator()); + } + + if (files.length > 0) { + for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) { + result.push(this.createOpenRecentMenuAction(files[i], 'openRecentFile', false)); + } + + result.push(new Separator()); + } + + return result; + } + + private getUpdateAction(): IAction | null { + const state = this.updateService.state; + + switch (state.type) { + case StateType.Uninitialized: + return null; + + case StateType.Idle: + const windowId = this.windowService.getCurrentWindowId(); + return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, true, () => + this.updateService.checkForUpdates({ windowId })); + + case StateType.CheckingForUpdates: + return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false); + + case StateType.AvailableForDownload: + return new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => + this.updateService.downloadUpdate()); + + case StateType.Downloading: + return new Action('update.downloading', nls.localize('DownloadingUpdate', "Downloading Update..."), undefined, false); + + case StateType.Downloaded: + return new Action('update.install', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => + this.updateService.applyUpdate()); + + case StateType.Updating: + return new Action('update.updating', nls.localize('installingUpdate', "Installing Update..."), undefined, false); + + case StateType.Ready: + return new Action('update.restart', nls.localize('restartToUpdate', "Restart to Update..."), undefined, true, () => + this.updateService.quitAndInstall()); + } + } + + private insertActionsBefore(nextAction: IAction, target: IAction[]): void { + switch (nextAction.id) { + case 'workbench.action.openRecent': + target.push(...this.getOpenRecentActions()); + break; + + case 'workbench.action.showAboutDialog': + if (!isMacintosh) { + const updateAction = this.getUpdateAction(); + if (updateAction) { + target.push(updateAction); + target.push(new Separator()); + } + } + + break; + + default: + break; + } + } + + private setupCustomMenubar(): void { + // Don't update while using the menu + if (this.isFocused) { + this.updatePending = true; + return; + } + + this.container.attributes['role'] = 'menubar'; + + const firstTimeSetup = this.customMenus === undefined; + if (firstTimeSetup) { + this.customMenus = []; + this.mnemonics = new Map(); + } + + let idx = 0; + + for (let menuTitle of Object.keys(this.topLevelMenus)) { + const menu: IMenu = this.topLevelMenus[menuTitle]; + let menuIndex = idx++; + const cleanMenuLabel = cleanMnemonic(this.topLevelTitles[menuTitle]); + + // Create the top level menu button element + if (firstTimeSetup) { + + const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': 0, 'aria-label': cleanMenuLabel, 'aria-haspopup': true }); + const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true }); + + buttonElement.appendChild(titleElement); + this.container.appendChild(buttonElement); + + this.customMenus.push({ + title: menuTitle, + buttonElement: buttonElement, + titleElement: titleElement + }); + } + + // Update the button label to reflect mnemonics + this.customMenus[menuIndex].titleElement.innerHTML = this.currentEnableMenuBarMnemonics ? + strings.escape(this.topLevelTitles[menuTitle]).replace(MENU_ESCAPED_MNEMONIC_REGEX, '') : + cleanMenuLabel; + + let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(this.topLevelTitles[menuTitle]); + + // Register mnemonics + if (mnemonicMatches) { + let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2]; + + if (firstTimeSetup) { + this.registerMnemonic(menuIndex, mnemonic); + } + + if (this.currentEnableMenuBarMnemonics) { + this.customMenus[menuIndex].buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase()); + } else { + this.customMenus[menuIndex].buttonElement.removeAttribute('aria-keyshortcuts'); + } + } + + // Update the menu actions + const updateActions = (menu: IMenu, target: IAction[]) => { + target.splice(0); + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + for (let action of actions) { + this.insertActionsBefore(action, target); + if (action instanceof SubmenuItemAction) { + const submenu = this.menuService.createMenu(action.item.submenu, this.contextKeyService); + const submenuActions = []; + updateActions(submenu, submenuActions); + target.push(new SubmenuAction(action.label, submenuActions)); + } else { + action.label = this.calculateActionLabel(action); + this.setCheckedStatus(action); + target.push(action); + } + } + + target.push(new Separator()); + } + + target.pop(); + }; + + this.customMenus[menuIndex].actions = []; + if (firstTimeSetup) { + this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); + } + + updateActions(menu, this.customMenus[menuIndex].actions); + + if (firstTimeSetup) { + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.KEY_UP, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.openedViaKeyboard = true; + this.focusState = MenubarState.OPEN; + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + })); + + Gesture.addTarget(this.customMenus[menuIndex].buttonElement); + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, EventType.Tap, (e: GestureEvent) => { + // Ignore this touch if the menu is touched + if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) { + return; + } + + this.ignoreNextMouseUp = false; + this.onMenuTriggered(menuIndex, true); + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_DOWN, (e) => { + if (!this.isOpen) { + // Open the menu with mouse down and ignore the following mouse up event + this.ignoreNextMouseUp = true; + this.onMenuTriggered(menuIndex, true); + } else { + this.ignoreNextMouseUp = false; + } + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_UP, (e) => { + if (!this.ignoreNextMouseUp) { + this.onMenuTriggered(menuIndex, true); + } else { + this.ignoreNextMouseUp = false; + } + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_ENTER, () => { + if (this.isOpen && !this.isCurrentMenu(menuIndex)) { + this.customMenus[menuIndex].buttonElement.focus(); + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex, false); + } else if (this.isFocused && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.customMenus[menuIndex].buttonElement.focus(); + } + })); + } + } + + if (firstTimeSetup) { + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + const key = !!e.key ? KeyCodeUtils.fromString(e.key) : KeyCode.Unknown; + + if (event.equals(KeyCode.LeftArrow) || (event.shiftKey && event.keyCode === KeyCode.Tab)) { + this.focusPrevious(); + } else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Tab)) { + this.focusNext(); + } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { + this.setUnfocusedState(); + } else if (!this.isOpen && !event.ctrlKey && this.currentEnableMenuBarMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) { + const menuIndex = this.mnemonics.get(key); + this.onMenuTriggered(menuIndex, false); + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + })); + + this._register(DOM.addDisposableListener(window, DOM.EventType.CLICK, () => { + // This click is outside the menubar so it counts as a focus out + if (this.isFocused) { + this.setUnfocusedState(); + } + })); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = event.relatedTarget as HTMLElement; + } + } + })); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = null; + this.setUnfocusedState(); + } + } + })); + + this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e) => { + if (!this.currentEnableMenuBarMnemonics || !e.altKey || e.ctrlKey) { + return; + } + + const key = KeyCodeUtils.fromString(e.key); + if (!this.mnemonics.has(key)) { + return; + } + + // Prevent conflicts with keybindings + const standardKeyboardEvent = new StandardKeyboardEvent(e); + const resolvedResult = this.keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target); + if (resolvedResult) { + return; + } + + this.mnemonicsInUse = true; + this.updateMnemonicVisibility(true); + + const menuIndex = this.mnemonics.get(key); + this.onMenuTriggered(menuIndex, false); + })); + } + } + + private onMenuTriggered(menuIndex: number, clicked: boolean) { + if (this.isOpen) { + if (this.isCurrentMenu(menuIndex)) { + this.setUnfocusedState(); + } else { + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex, this.openedViaKeyboard); + } + } else { + this.focusedMenu = { index: menuIndex }; + this.openedViaKeyboard = !clicked; + this.focusState = MenubarState.OPEN; + } + } + + private focusPrevious(): void { + + if (!this.focusedMenu) { + return; + } + + let newFocusedIndex = (this.focusedMenu.index - 1 + this.customMenus.length) % this.customMenus.length; + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.focus(); + } + } + + private focusNext(): void { + if (!this.focusedMenu) { + return; + } + + let newFocusedIndex = (this.focusedMenu.index + 1) % this.customMenus.length; + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + this.customMenus[newFocusedIndex].buttonElement.focus(); + } + } + + private getMenubarKeybinding(id: string): IMenubarKeybinding { + const binding = this.keybindingService.lookupKeybinding(id); + if (!binding) { + return null; + } + + // first try to resolve a native accelerator + const electronAccelerator = binding.getElectronAccelerator(); + if (electronAccelerator) { + return { id, label: electronAccelerator, isNative: true }; + } + + // we need this fallback to support keybindings that cannot show in electron menus (e.g. chords) + const acceleratorLabel = binding.getLabel(); + if (acceleratorLabel) { + return { id, label: acceleratorLabel, isNative: false }; + } + + return null; + } + + private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu) { + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + actions.forEach(menuItem => { + + if (menuItem instanceof SubmenuItemAction) { + const submenu = { items: [] }; + this.populateMenuItems(this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService), submenu); + + let menubarSubmenuItem: IMenubarMenuItemSubmenu = { + id: menuItem.id, + label: menuItem.label, + submenu: submenu + }; + + menuToPopulate.items.push(menubarSubmenuItem); + } else { + let menubarMenuItem: IMenubarMenuItemAction = { + id: menuItem.id, + label: menuItem.label, + checked: menuItem.checked, + enabled: menuItem.enabled, + keybinding: this.getMenubarKeybinding(menuItem.id) + }; + + this.setCheckedStatus(menubarMenuItem); + menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem); + + menuToPopulate.items.push(menubarMenuItem); + } + }); + + menuToPopulate.items.push({ id: 'vscode.menubar.separator' }); + } + + if (menuToPopulate.items.length > 0) { + menuToPopulate.items.pop(); + } + } + + // private getAdditionalKeybindings(): Array { + // const keybindings = []; + // if (isMacintosh) { + // keybindings.push(this.getMenubarKeybinding('workbench.action.quit')); + // } + + // return keybindings; + // } + + // private getMenubarMenus(menubarData: IMenubarData): boolean { + // if (!menubarData) { + // return false; + // } + + // for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { + // const menu = this.topLevelMenus[topLevelMenuName]; + // let menubarMenu: IMenubarMenu = { items: [] }; + // this.populateMenuItems(menu, menubarMenu); + // if (menubarMenu.items.length === 0) { + // // Menus are incomplete + // return false; + // } + // menubarData[topLevelMenuName] = menubarMenu; + // } + + // return true; + // } + + private isCurrentMenu(menuIndex: number): boolean { + if (!this.focusedMenu) { + return false; + } + + return this.focusedMenu.index === menuIndex; + } + + private cleanupCustomMenu(): void { + if (this.focusedMenu) { + // Remove focus from the menus first + this.customMenus[this.focusedMenu.index].buttonElement.focus(); + + if (this.focusedMenu.holder) { + DOM.removeClass(this.focusedMenu.holder.parentElement, 'open'); + this.focusedMenu.holder.remove(); + } + + if (this.focusedMenu.widget) { + this.focusedMenu.widget.dispose(); + } + + this.focusedMenu = { index: this.focusedMenu.index }; + } + } + + private showCustomMenu(menuIndex: number, selectFirst = true): void { + const customMenu = this.customMenus[menuIndex]; + const menuHolder = $('div.menubar-menu-items-holder'); + + DOM.addClass(customMenu.buttonElement, 'open'); + menuHolder.style.top = `${this.container.clientHeight}px`; + menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`; + + customMenu.buttonElement.appendChild(menuHolder); + + let menuOptions: IMenuOptions = { + getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id), + actionRunner: this.actionRunner, + enableMnemonics: this.mnemonicsInUse && this.currentEnableMenuBarMnemonics, + ariaLabel: customMenu.buttonElement.attributes['aria-label'].value + }; + + let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions)); + + this._register(menuWidget.onDidCancel(() => { + this.focusState = MenubarState.FOCUSED; + })); + + this._register(menuWidget.onDidBlur(() => { + setTimeout(() => { + this.cleanupCustomMenu(); + }, 100); + })); + + menuWidget.focus(selectFirst); + + this.focusedMenu = { + index: menuIndex, + holder: menuHolder, + widget: menuWidget + }; + } + + public get onVisibilityChange(): Event { + return this._onVisibilityChange.event; + } + + public layout(dimension: DOM.Dimension) { + if (this.container) { + this.container.style.height = `${dimension.height}px`; + } + + if (!this.isVisible) { + this.hideMenubar(); + } else { + this.showMenubar(); + } + } + + public getMenubarItemsDimensions(): DOM.Dimension { + if (this.customMenus) { + const left = this.customMenus[0].buttonElement.getBoundingClientRect().left; + const right = this.customMenus[this.customMenus.length - 1].buttonElement.getBoundingClientRect().right; + return new DOM.Dimension(right - left, this.container.clientHeight); + } + + return new DOM.Dimension(0, 0); + } + + public create(parent: HTMLElement): HTMLElement { + this.container = parent; + + // Build the menubar + if (this.container) { + this.doSetupMenubar(); + + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + this.setUnfocusedState(); + } + } + + return this.container; + } +} + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); + if (menubarActiveWindowFgColor) { + collector.addRule(` + .monaco-workbench .menubar > .menubar-menu-button { + color: ${menubarActiveWindowFgColor}; + } + `); + } + + const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); + if (menubarInactiveWindowFgColor) { + collector.addRule(` + .monaco-workbench .menubar.inactive > .menubar-menu-button { + color: ${menubarInactiveWindowFgColor}; + } + `); + } + + + const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); + if (menubarSelectedFgColor) { + collector.addRule(` + .monaco-workbench .menubar > .menubar-menu-button.open, + .monaco-workbench .menubar > .menubar-menu-button:focus, + .monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover { + color: ${menubarSelectedFgColor}; + } + `); + } + + const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND); + if (menubarSelectedBgColor) { + collector.addRule(` + .monaco-workbench .menubar > .menubar-menu-button.open, + .monaco-workbench .menubar > .menubar-menu-button:focus, + .monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover { + background-color: ${menubarSelectedBgColor}; + } + `); + } + + const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER); + if (menubarSelectedBorderColor) { + collector.addRule(` + .monaco-workbench .menubar > .menubar-menu-button:hover { + outline: dashed 1px; + } + + .monaco-workbench .menubar > .menubar-menu-button.open, + .monaco-workbench .menubar > .menubar-menu-button:focus { + outline: solid 1px; + } + + .monaco-workbench .menubar > .menubar-menu-button.open, + .monaco-workbench .menubar > .menubar-menu-button:focus, + .monaco-workbench .menubar > .menubar-menu-button:hover { + outline-offset: -1px; + outline-color: ${menubarSelectedBorderColor}; + } + `); + } + + const menuShadow = theme.getColor('widget.shadow'); + if (menuShadow) { + collector.addRule(` + .monaco-shell .monaco-workbench .monaco-menu-container { + box-shadow: 0 2px 4px ${menuShadow}; + } + `); + } + + const menuBgColor = theme.getColor(MENU_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item { + background-color: ${menuBgColor}; + } + `); + } + + let menuFgColor = theme.getColor(MENU_FOREGROUND); + if (!menuFgColor) { + menuFgColor = theme.getColor(foreground); + } + + if (menuFgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item { + color: ${menuFgColor}; + } + + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item .menu-item-check, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item .submenu-indicator { + background-color: ${menuFgColor}; + } + `); + } + + const selectedMenuItemBgColor = theme.getColor(MENU_SELECTION_BACKGROUND); + if (menuBgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + background-color: ${selectedMenuItemBgColor}; + } + `); + } + + const selectedMenuItemFgColor = theme.getColor(MENU_SELECTION_FOREGROUND); + if (selectedMenuItemFgColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + color: ${selectedMenuItemFgColor}; + } + + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused .action-menu-item .menu-item-check, + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused .action-menu-item .submenu-indicator { + background-color: ${selectedMenuItemFgColor}; + } + `); + } + + const selectedMenuItemBorderColor = theme.getColor(MENU_SELECTION_BORDER); + if (selectedMenuItemBorderColor) { + collector.addRule(` + .monaco-shell .monaco-menu .monaco-action-bar.vertical .action-item.focused { + border: 1px solid ${selectedMenuItemBorderColor}; + } + `); + } +}); + +type ModifierKey = 'alt' | 'ctrl' | 'shift'; + +interface IModifierKeyStatus { + altKey: boolean; + shiftKey: boolean; + ctrlKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; +} + + +class ModifierKeyEmitter extends Emitter { + + private _subscriptions: IDisposable[] = []; + private _keyStatus: IModifierKeyStatus; + private static instance: ModifierKeyEmitter; + + private constructor(windowService: IWindowService) { + super(); + + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false + }; + + this._subscriptions.push(domEvent(document.body, 'keydown')(e => { + const event = new StandardKeyboardEvent(e); + + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = 'alt'; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = 'ctrl'; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = 'shift'; + } else if (event.keyCode !== KeyCode.Alt) { + this._keyStatus.lastKeyPressed = undefined; + } else { + return; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this.fire(this._keyStatus); + } + })); + this._subscriptions.push(domEvent(document.body, 'keyup')(e => { + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = 'alt'; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = 'ctrl'; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = 'shift'; + } else { + this._keyStatus.lastKeyReleased = undefined; + } + + if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this.fire(this._keyStatus); + } + })); + this._subscriptions.push(domEvent(document.body, 'mousedown')(e => { + this._keyStatus.lastKeyPressed = undefined; + })); + + this._subscriptions.push(windowService.onDidChangeFocus(focused => { + if (!focused) { + this._keyStatus.lastKeyPressed = undefined; + this._keyStatus.lastKeyReleased = undefined; + this._keyStatus.altKey = false; + this._keyStatus.shiftKey = false; + this._keyStatus.shiftKey = false; + + this.fire(this._keyStatus); + } + })); + } + + static getInstance(windowService: IWindowService) { + if (!ModifierKeyEmitter.instance) { + ModifierKeyEmitter.instance = new ModifierKeyEmitter(windowService); + } + + return ModifierKeyEmitter.instance; + } + + dispose() { + super.dispose(); + this._subscriptions = dispose(this._subscriptions); + } +} diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 63be5a66917..3c10e3bb14d 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -7,12 +7,11 @@ import 'vs/css!./media/titlebarpart'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Builder, $ } from 'vs/base/browser/builder'; import * as paths from 'vs/base/common/paths'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; import { getZoomFactor } from 'vs/base/browser/browser'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService, MenuBarVisibility } from 'vs/platform/windows/common/windows'; import * as errors from 'vs/base/common/errors'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -21,20 +20,25 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; -import * as labels from 'vs/base/common/labels'; import { EditorInput, toResource, Verbosity } from 'vs/workbench/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, hide, show, removeClass, addClass, append, $, addDisposableListener } from 'vs/base/browser/dom'; +import { MenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; +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'; export class TitlebarPart extends Part implements ITitleService { - public _serviceBrand: any; + _serviceBrand: any; private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]"); private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]"); @@ -42,12 +46,27 @@ 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: Builder; - private title: Builder; + private titleContainer: HTMLElement; + private title: HTMLElement; + private dragRegion: HTMLElement; + private windowControls: HTMLElement; + private maxRestoreControl: HTMLElement; + private appIcon: HTMLElement; + private menubarPart: MenubarControl; + private menubar: HTMLElement; + private resizer: HTMLElement; + private pendingTitle: string; - private initialTitleFontSize: number; private representedFileName: string; + private initialSizing: { + titleFontSize?: number; + titlebarHeight?: number; + controlsWidth?: number; + appIconSize?: number; + appIconWidth?: number; + } = Object.create(null); + private isInactive: boolean; private properties: ITitleProperties; @@ -62,7 +81,9 @@ export class TitlebarPart extends Part implements ITitleService { @IEditorService private editorService: IEditorService, @IEnvironmentService private environmentService: IEnvironmentService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IThemeService themeService: IThemeService + @IInstantiationService private instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @ILabelService private labelService: ILabelService ) { super(id, { hasTitle: false }, themeService); @@ -73,13 +94,13 @@ export class TitlebarPart extends Part implements ITitleService { } private registerListeners(): void { - this.toUnbind.push(addDisposableListener(window, EventType.BLUR, () => this.onBlur())); - this.toUnbind.push(addDisposableListener(window, EventType.FOCUS, () => this.onFocus())); - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); - this.toUnbind.push(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChange())); - this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle()))); - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle()))); - this.toUnbind.push(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle()))); + this._register(this.windowService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur())); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); + this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChange())); + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle()))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle()))); + this._register(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle()))); + this._register(this.labelService.onDidRegisterFormatter(() => this.setTitle(this.getWindowTitle()))); } private onBlur(): void { @@ -98,6 +119,25 @@ export class TitlebarPart extends Part implements ITitleService { } } + private onMenubarVisibilityChanged(visible: boolean) { + if (isWindows || isLinux) { + // Hide title when toggling menu bar + if (this.configurationService.getValue('window.menuBarVisibility') === 'toggle' && visible) { + this.title.style.visibility = 'hidden'; + + // Hack to fix issue #52522 with layered webkit-app-region elements appearing under cursor + hide(this.dragRegion); + setTimeout(() => show(this.dragRegion), 50); + } else { + this.title.style.visibility = null; + } + } + } + + onMenubarVisibilityChange(): Event { + return this.menubarPart.onVisibilityChange; + } + private onActiveEditorChange(): void { // Dispose old listeners @@ -156,7 +196,7 @@ export class TitlebarPart extends Part implements ITitleService { return title; } - public updateProperties(properties: ITitleProperties): void { + updateProperties(properties: ITitleProperties): void { const isAdmin = typeof properties.isAdmin === 'boolean' ? properties.isAdmin : this.properties.isAdmin; const isPure = typeof properties.isPure === 'boolean' ? properties.isPure : this.properties.isPure; @@ -202,16 +242,16 @@ export class TitlebarPart extends Part implements ITitleService { const activeEditorShort = editor ? editor.getTitle(Verbosity.SHORT) : ''; const activeEditorMedium = editor ? editor.getTitle(Verbosity.MEDIUM) : activeEditorShort; const activeEditorLong = editor ? editor.getTitle(Verbosity.LONG) : activeEditorMedium; - const rootName = workspace.name; - const rootPath = root ? labels.getPathLabel(root, void 0, this.environmentService) : ''; + const rootName = this.labelService.getWorkspaceLabel(workspace); + const rootPath = root ? this.labelService.getUriLabel(root) : ''; const folderName = folder ? folder.name : ''; - const folderPath = folder ? labels.getPathLabel(folder.uri, void 0, this.environmentService) : ''; + const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : ''; const dirty = editor && editor.isDirty() ? TitlebarPart.TITLE_DIRTY : ''; const appName = this.environmentService.appNameLong; const separator = TitlebarPart.TITLE_SEPARATOR; const titleTemplate = this.configurationService.getValue('window.title'); - return labels.template(titleTemplate, { + return template(titleTemplate, { activeEditorShort, activeEditorLong, activeEditorMedium, @@ -225,43 +265,136 @@ export class TitlebarPart extends Part implements ITitleService { }); } - public createContentArea(parent: HTMLElement): HTMLElement { - this.titleContainer = $(parent); + createContentArea(parent: HTMLElement): HTMLElement { + this.titleContainer = parent; + + // Draggable region that we can manipulate for #52522 + this.dragRegion = append(this.titleContainer, $('div.titlebar-drag-region')); + + // App Icon (Windows/Linux) + if (!isMacintosh) { + this.appIcon = append(this.titleContainer, $('div.window-appicon')); + } + + // 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.setAttribute('role', 'menubar'); + + this.menubarPart.create(this.menubar); + + if (!isMacintosh) { + this._register(this.menubarPart.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); + } // Title - this.title = $(this.titleContainer).div({ class: 'window-title' }); + this.title = append(this.titleContainer, $('div.window-title')); if (this.pendingTitle) { - this.title.text(this.pendingTitle); + this.title.innerText = this.pendingTitle; + } else { + this.setTitle(this.getWindowTitle()); } // Maximize/Restore on doubleclick - this.titleContainer.on(EventType.DBLCLICK, (e) => { - EventHelper.stop(e); - - this.onTitleDoubleclick(); - }); - - // Context menu on title - this.title.on([EventType.CONTEXT_MENU, EventType.MOUSE_DOWN], (e: MouseEvent) => { - if (e.type === EventType.CONTEXT_MENU || e.metaKey) { + if (isMacintosh) { + this._register(addDisposableListener(this.titleContainer, EventType.DBLCLICK, e => { EventHelper.stop(e); - this.onContextMenu(e); - } + this.onTitleDoubleclick(); + })); + } + + // Context menu on title + [EventType.CONTEXT_MENU, EventType.MOUSE_DOWN].forEach(event => { + this._register(addDisposableListener(this.title, event, e => { + if (e.type === EventType.CONTEXT_MENU || e.metaKey) { + EventHelper.stop(e); + + this.onContextMenu(e); + } + })); }); + // Window Controls (Windows/Linux) + if (!isMacintosh) { + this.windowControls = append(this.titleContainer, $('div.window-controls-container')); + + + // Minimize + const minimizeIconContainer = append(this.windowControls, $('div.window-icon-bg')); + const minimizeIcon = append(minimizeIconContainer, $('div.window-icon')); + addClass(minimizeIcon, 'window-minimize'); + this._register(addDisposableListener(minimizeIcon, EventType.CLICK, e => { + this.windowService.minimizeWindow(); + })); + + // Restore + const restoreIconContainer = append(this.windowControls, $('div.window-icon-bg')); + this.maxRestoreControl = append(restoreIconContainer, $('div.window-icon')); + addClass(this.maxRestoreControl, 'window-max-restore'); + this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, e => { + this.windowService.isMaximized().then((maximized) => { + if (maximized) { + return this.windowService.unmaximizeWindow(); + } + + return this.windowService.maximizeWindow(); + }); + })); + + // Close + const closeIconContainer = append(this.windowControls, $('div.window-icon-bg')); + addClass(closeIconContainer, 'window-close-bg'); + const closeIcon = append(closeIconContainer, $('div.window-icon')); + addClass(closeIcon, 'window-close'); + this._register(addDisposableListener(closeIcon, EventType.CLICK, e => { + this.windowService.closeWindow(); + })); + + // Resizer + this.resizer = append(this.titleContainer, $('div.resizer')); + + const isMaximized = this.windowService.getConfiguration().maximized ? true : false; + this.onDidChangeMaximized(isMaximized); + this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this); + } + // 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.titleContainer.on([EventType.MOUSE_DOWN], () => { + this._register(addDisposableListener(this.titleContainer, EventType.MOUSE_DOWN, e => { + if (e.target && isAncestor(e.target as HTMLElement, this.menubar)) { + return; + } + const active = document.activeElement; setTimeout(() => { if (active instanceof HTMLElement) { active.focus(); } }, 0 /* need a timeout because we are in capture phase */); - }, void 0, true /* use capture to know the currently active element properly */); + }, true /* use capture to know the currently active element properly */)); - return this.titleContainer.getHTMLElement(); + return this.titleContainer; + } + + private onDidChangeMaximized(maximized: boolean) { + if (this.maxRestoreControl) { + if (maximized) { + removeClass(this.maxRestoreControl, 'window-maximize'); + addClass(this.maxRestoreControl, 'window-unmaximize'); + } else { + removeClass(this.maxRestoreControl, 'window-unmaximize'); + addClass(this.maxRestoreControl, 'window-maximize'); + } + } + + if (this.resizer) { + if (maximized) { + hide(this.resizer); + } else { + show(this.resizer); + } + } } protected updateStyles(): void { @@ -269,11 +402,25 @@ export class TitlebarPart extends Part implements ITitleService { // Part container if (this.titleContainer) { - this.titleContainer.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND)); - this.titleContainer.style('background-color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND)); + if (this.isInactive) { + addClass(this.titleContainer, 'inactive'); + } else { + removeClass(this.titleContainer, 'inactive'); + } + + const titleBackground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND); + this.titleContainer.style.backgroundColor = titleBackground; + if (Color.fromHex(titleBackground).isLighter()) { + addClass(this.titleContainer, 'light'); + } else { + removeClass(this.titleContainer, 'light'); + } + + const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND); + this.titleContainer.style.color = titleForeground; const titleBorder = this.getColor(TITLE_BAR_BORDER); - this.titleContainer.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null); + this.titleContainer.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : null; } } @@ -315,9 +462,9 @@ export class TitlebarPart extends Part implements ITitleService { let label: string; if (!isFile) { - label = labels.getBaseLabel(paths.dirname(path)); + label = getBaseLabel(paths.dirname(path)); } else { - label = labels.getBaseLabel(path); + label = getBaseLabel(path); } actions.push(new ShowItemInFolderAction(path, label || paths.sep, this.windowsService)); @@ -327,26 +474,86 @@ export class TitlebarPart extends Part implements ITitleService { return actions; } - public setTitle(title: string): void { + setTitle(title: string): void { // Always set the native window title to identify us properly to the OS window.document.title = title; // Apply if we can if (this.title) { - this.title.text(title); + this.title.innerText = title; } else { this.pendingTitle = title; } } - public layout(dimension: Dimension): Dimension[] { - - // To prevent zooming we need to adjust the font size with the zoom factor - if (typeof this.initialTitleFontSize !== 'number') { - this.initialTitleFontSize = parseInt(this.titleContainer.getComputedStyle().fontSize, 10); + private updateLayout(dimension: Dimension) { + // Store initital title sizing if we need to prevent zooming + if (typeof this.initialSizing.titleFontSize !== 'number') { + this.initialSizing.titleFontSize = parseInt(getComputedStyle(this.title).fontSize, 10); } - this.titleContainer.style({ fontSize: `${this.initialTitleFontSize / getZoomFactor()}px` }); + + if (typeof this.initialSizing.titlebarHeight !== 'number') { + this.initialSizing.titlebarHeight = parseInt(getComputedStyle(this.title).height, 10); + } + + // Only prevent zooming behavior on macOS or when the menubar is not visible + if (isMacintosh || this.configurationService.getValue('window.menuBarVisibility') === 'hidden') { + // To prevent zooming we need to adjust the font size with the zoom factor + const newHeight = this.initialSizing.titlebarHeight / getZoomFactor(); + this.title.style.fontSize = `${this.initialSizing.titleFontSize / getZoomFactor()}px`; + this.title.style.lineHeight = `${newHeight}px`; + + // Windows/Linux specific layout + if (isWindows || isLinux) { + if (typeof this.initialSizing.controlsWidth !== 'number') { + this.initialSizing.controlsWidth = parseInt(getComputedStyle(this.windowControls).width, 10); + } + + const appIconComputedStyles = getComputedStyle(this.appIcon); + if (typeof this.initialSizing.appIconWidth !== 'number') { + this.initialSizing.appIconWidth = parseInt(appIconComputedStyles.width, 10); + } + + if (typeof this.initialSizing.appIconSize !== 'number') { + this.initialSizing.appIconSize = parseInt(appIconComputedStyles.backgroundSize, 10); + } + + const currentAppIconHeight = parseInt(appIconComputedStyles.height, 10); + const newControlsWidth = this.initialSizing.controlsWidth / getZoomFactor(); + const newAppIconWidth = this.initialSizing.appIconWidth / getZoomFactor(); + const newAppIconSize = this.initialSizing.appIconSize / getZoomFactor(); + + // Adjust app icon mimic menubar + this.appIcon.style.width = `${newAppIconWidth}px`; + this.appIcon.style.backgroundSize = `${newAppIconSize}px`; + this.appIcon.style.paddingTop = `${(newHeight - currentAppIconHeight) / 2.0}px`; + this.appIcon.style.paddingBottom = `${(newHeight - currentAppIconHeight) / 2.0}px`; + + // Adjust windows controls + this.windowControls.style.width = `${newControlsWidth}px`; + } + } else { + // We need to undo zoom prevention + this.title.style.fontSize = null; + this.title.style.lineHeight = null; + + this.appIcon.style.width = null; + this.appIcon.style.backgroundSize = null; + this.appIcon.style.paddingTop = null; + this.appIcon.style.paddingBottom = null; + + this.windowControls.style.width = null; + } + + if (this.menubarPart) { + const menubarDimension = new Dimension(undefined, dimension.height); + this.menubarPart.layout(menubarDimension); + } + } + + layout(dimension: Dimension): Dimension[] { + this.updateLayout(dimension); return super.layout(dimension); } @@ -358,7 +565,27 @@ class ShowItemInFolderAction extends Action { super('showItemInFolder.action.id', label); } - public run(): TPromise { + run(): TPromise { return this.windowsService.showItemInFolder(this.path); } -} \ No newline at end of file +} + +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 { + background-color: ${titlebarActiveFg}; + } + `); + } + + const titlebarInactiveFg = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); + if (titlebarInactiveFg) { + collector.addRule(` + .monaco-workbench > .part.titlebar.inactive > .window-controls-container .window-icon { + background-color: ${titlebarInactiveFg}; + } + `); + } +}); diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 04a9eabae59..212fbe26b9d 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/views'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, empty as EmptyDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, IActionItem, ActionRunner } from 'vs/base/common/actions'; @@ -14,26 +14,26 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuItemActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewsService, ITreeViewer, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ICustomViewDescriptor, ViewsRegistry } from 'vs/workbench/common/views'; +import { IViewsService, ITreeViewer, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ICustomViewDescriptor, ViewsRegistry, ViewContainer } from 'vs/workbench/common/views'; import { IViewletViewOptions, FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ProgressLocation, IProgressService2 } from 'vs/platform/progress/common/progress'; +import { IProgressService2 } from 'vs/workbench/services/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as DOM from 'vs/base/browser/dom'; -import * as errors from 'vs/base/common/errors'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionItemProvider, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { basename } from 'vs/base/common/paths'; import { LIGHT, FileThemeIcon, FolderThemeIcon } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; export class CustomTreeViewPanel extends ViewletPanel { @@ -108,7 +108,7 @@ export class CustomTreeViewPanel extends ViewletPanel { class TitleMenus implements IDisposable { private disposables: IDisposable[] = []; - private titleDisposable: IDisposable = EmptyDisposable; + private titleDisposable: IDisposable = Disposable.None; private titleActions: IAction[] = []; private titleSecondaryActions: IAction[] = []; @@ -122,7 +122,7 @@ class TitleMenus implements IDisposable { ) { if (this.titleDisposable) { this.titleDisposable.dispose(); - this.titleDisposable = EmptyDisposable; + this.titleDisposable = Disposable.None; } const _contextKeyService = this.contextKeyService.createScoped(); @@ -192,18 +192,27 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { private _onDidChangeSelection: Emitter = this._register(new Emitter()); readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + private _onDidChangeVisibility: Emitter = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + constructor( private id: string, - private progressLocation: ProgressLocation, + private container: ViewContainer, @IExtensionService private extensionService: IExtensionService, @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IInstantiationService private instantiationService: IInstantiationService, - @ICommandService private commandService: ICommandService + @ICommandService private commandService: ICommandService, + @IConfigurationService private configurationService: IConfigurationService ) { super(); this.root = new Root(); this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('explorer.decorations')) { + this.doRefresh([this.root]); /** soft refresh **/ + } + })); } get dataProvider(): ITreeViewDataProvider { @@ -238,7 +247,12 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { return this._hasIconForLeafNode; } + get visible(): boolean { + return this.isVisible; + } + setVisibility(isVisible: boolean): void { + isVisible = !!isVisible; if (this.isVisible === isVisible) { return; } @@ -266,6 +280,8 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { this.elementsToRefresh = []; } } + + this._onDidChangeVisibility.fire(this.isVisible); } focus(): void { @@ -273,7 +289,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { // Make sure the current selected element is revealed const selectedElement = this.tree.getSelection()[0]; if (selectedElement) { - this.tree.reveal(selectedElement, 0.5).done(null, errors.onUnexpectedError); + this.tree.reveal(selectedElement, 0.5); } // Pass Focus to Viewer @@ -292,7 +308,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { this.treeContainer = DOM.$('.tree-explorer-viewlet-tree-view'); const actionItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuItemActionItem, action) : undefined; const menus = this.instantiationService.createInstance(TreeMenus, this.id); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.progressLocation); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.container); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, menus, actionItemProvider); const controller = this.instantiationService.createInstance(TreeController, this.id, menus); this.tree = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, this.treeContainer, { dataSource, renderer, controller }, {}); @@ -336,13 +352,15 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { return TPromise.as(null); } - reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean }): TPromise { + reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean, focus?: boolean }): TPromise { if (this.tree && this.isVisible) { - options = options ? options : { select: true }; + options = options ? options : { select: false, focus: false }; + const select = isUndefinedOrNull(options.select) ? false : options.select; + const focus = isUndefinedOrNull(options.focus) ? false : options.focus; + const root: Root = this.tree.getInput(); const promise = root.children ? TPromise.as(null) : this.refresh(); // Refresh if root is not populated return promise.then(() => { - const select = isUndefinedOrNull(options.select) ? true : options.select; var result = TPromise.as(null); parentChain.forEach((e) => { result = result.then(() => this.tree.expand(e)); @@ -352,6 +370,10 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { if (select) { this.tree.setSelection([item], { source: 'api' }); } + if (focus) { + this.focus(); + this.tree.setFocus(item); + } }); }); } @@ -373,7 +395,7 @@ export class CustomTreeViewer extends Disposable implements ITreeViewer { } private onSelection({ payload }: any): void { - if (payload && payload.source === 'api') { + if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { return; } const selection: ITreeItem = this.tree.getSelection()[0]; @@ -395,37 +417,36 @@ class TreeDataSource implements IDataSource { constructor( private treeView: ITreeViewer, - private location: ProgressLocation, + private container: ViewContainer, @IProgressService2 private progressService: IProgressService2 ) { } - public getId(tree: ITree, node: ITreeItem): string { + getId(tree: ITree, node: ITreeItem): string { return node.handle; } - public hasChildren(tree: ITree, node: ITreeItem): boolean { + hasChildren(tree: ITree, node: ITreeItem): boolean { return this.treeView.dataProvider && node.collapsibleState !== TreeItemCollapsibleState.None; } - public getChildren(tree: ITree, node: ITreeItem): TPromise { + getChildren(tree: ITree, node: ITreeItem): TPromise { if (this.treeView.dataProvider) { - return this.location ? this.progressService.withProgress({ location: this.location }, () => this.treeView.dataProvider.getChildren(node)) : this.treeView.dataProvider.getChildren(node); + return this.progressService.withProgress({ location: this.container }, () => this.treeView.dataProvider.getChildren(node)); } return TPromise.as([]); } - public shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { + shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { return node.collapsibleState === TreeItemCollapsibleState.Expanded; } - public getParent(tree: ITree, node: any): TPromise { + getParent(tree: ITree, node: any): TPromise { return TPromise.as(null); } } interface ITreeExplorerTemplateData { - label: HTMLElement; resourceLabel: ResourceLabel; icon: HTMLElement; actionBar: ActionBar; @@ -442,57 +463,54 @@ class TreeRenderer implements IRenderer { private menus: TreeMenus, private actionItemProvider: IActionItemProvider, @IInstantiationService private instantiationService: IInstantiationService, - @IWorkbenchThemeService private themeService: IWorkbenchThemeService + @IWorkbenchThemeService private themeService: IWorkbenchThemeService, + @IConfigurationService private configurationService: IConfigurationService, ) { } - public getHeight(tree: ITree, element: any): number { + getHeight(tree: ITree, element: any): number { return TreeRenderer.ITEM_HEIGHT; } - public getTemplateId(tree: ITree, element: any): string { + getTemplateId(tree: ITree, element: any): string { return TreeRenderer.TREE_TEMPLATE_ID; } - public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData { + renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData { DOM.addClass(container, 'custom-view-tree-node-item'); const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); - const label = DOM.append(container, DOM.$('.custom-view-tree-node-item-label')); const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, {}); - const actionsContainer = DOM.append(container, DOM.$('.actions')); + DOM.addClass(resourceLabel.element, 'custom-view-tree-node-item-resourceLabel'); + const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); const actionBar = new ActionBar(actionsContainer, { actionItemProvider: this.actionItemProvider, actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) }); - return { label, resourceLabel, icon, actionBar, aligner: new Aligner(container, tree, this.themeService) }; + return { resourceLabel, icon, actionBar, aligner: new Aligner(container, tree, this.themeService) }; } - public renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void { + renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void { const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; const label = node.label ? node.label : resource ? basename(resource.path) : ''; const icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; + const iconUrl = icon ? URI.revive(icon) : null; + const title = node.tooltip ? node.tooltip : resource ? void 0 : label; // reset templateData.resourceLabel.clear(); templateData.actionBar.clear(); - templateData.label.textContent = ''; - DOM.removeClass(templateData.label, 'custom-view-tree-node-item-label'); - DOM.removeClass(templateData.resourceLabel.element, 'custom-view-tree-node-item-resourceLabel'); - if ((resource || node.themeIcon) && !icon) { - const title = node.tooltip ? node.tooltip : resource ? void 0 : label; - templateData.resourceLabel.setLabel({ name: label, resource: resource ? resource : URI.parse('_icon_resource') }, { fileKind: this.getFileKind(node), title }); - DOM.addClass(templateData.resourceLabel.element, 'custom-view-tree-node-item-resourceLabel'); + if (resource || node.themeIcon) { + const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + templateData.resourceLabel.setLabel({ name: label, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); } else { - templateData.label.textContent = label; - DOM.addClass(templateData.label, 'custom-view-tree-node-item-label'); - templateData.label.title = typeof node.tooltip === 'string' ? node.tooltip : label; + templateData.resourceLabel.setLabel({ name: label }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); } - templateData.icon.style.backgroundImage = icon ? `url('${icon}')` : ''; - DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!icon); + templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; + DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl); templateData.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); @@ -511,7 +529,7 @@ class TreeRenderer implements IRenderer { return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; } - public disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void { + disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void { templateData.resourceLabel.dispose(); templateData.actionBar.dispose(); templateData.aligner.dispose(); @@ -586,7 +604,11 @@ class TreeController extends WorkbenchTreeController { super({}, configurationService); } - public onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { + protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { + return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); + } + + onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { event.preventDefault(); event.stopPropagation(); diff --git a/src/vs/workbench/browser/parts/views/media/panelviewlet.css b/src/vs/workbench/browser/parts/views/media/panelviewlet.css index adddb24399d..9691ecbdeb7 100644 --- a/src/vs/workbench/browser/parts/views/media/panelviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/panelviewlet.css @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-panel-view .panel > .panel-header > .title { +.monaco-panel-view .panel > .panel-header h3.title { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + font-size: 11px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; } diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 7d9c1184814..2bffac61f30 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -58,6 +58,10 @@ display: inline-block; } +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row { + padding-right: 12px; +} + .tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item { display: flex; height: 22px; @@ -68,8 +72,7 @@ flex-wrap: nowrap } -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel, -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-label { +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel { flex: 1; text-overflow: ellipsis; overflow: hidden; @@ -85,18 +88,25 @@ -webkit-font-smoothing: antialiased; } -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .actions { - display: none; - padding-right: 6px; +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container { + flex: 1; } -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row:hover .custom-view-tree-node-item > .actions, -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.selected .custom-view-tree-node-item > .actions, -.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.focused .custom-view-tree-node-item > .actions { +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after { + padding-right: 0px; +} + +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { + display: none; +} + +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, +.tree-explorer-viewlet-tree-view .monaco-tree .monaco-tree-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { display: block; } -.tree-explorer-viewlet-tree-view .monaco-tree .custom-view-tree-node-item > .actions .action-label { +.tree-explorer-viewlet-tree-view .monaco-tree .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label { width: 16px; height: 100%; background-position: 50% 50%; diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 12297203748..a647bf39021 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -27,6 +27,7 @@ import { PanelView, IPanelViewOptions, IPanelOptions, Panel } from 'vs/base/brow import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IView } from 'vs/workbench/common/views'; export interface IPanelColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -41,14 +42,17 @@ export interface IViewletPanelOptions extends IPanelOptions { title: string; } -export abstract class ViewletPanel extends Panel { +export abstract class ViewletPanel extends Panel implements IView { private static AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; private _onDidFocus = new Emitter(); readonly onDidFocus: Event = this._onDidFocus.event; - private _onDidChangeTitleArea = new Emitter(); + private _onDidBlur = new Emitter(); + readonly onDidBlur: Event = this._onDidBlur.event; + + protected _onDidChangeTitleArea = new Emitter(); readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; private _isVisible: boolean; @@ -90,12 +94,13 @@ export abstract class ViewletPanel extends Panel { const focusTracker = trackFocus(this.element); this.disposables.push(focusTracker); this.disposables.push(focusTracker.onDidFocus(() => this._onDidFocus.fire())); + this.disposables.push(focusTracker.onDidBlur(() => this._onDidBlur.fire())); } protected renderHeader(container: HTMLElement): void { this.headerContainer = container; - this.renderHeaderTitle(container); + this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); this.toolbar = new ToolBar(actions, this.contextMenuService, { @@ -114,8 +119,8 @@ export abstract class ViewletPanel extends Panel { this.updateActionsVisibility(); } - protected renderHeaderTitle(container: HTMLElement): void { - append(container, $('.title', null, this.title)); + protected renderHeaderTitle(container: HTMLElement, title: string): void { + append(container, $('h3.title', null, title)); } focus(): void { @@ -202,12 +207,12 @@ export class PanelViewlet extends Viewlet { super(id, partService, telemetryService, themeService); } - async create(parent: HTMLElement): TPromise { - super.create(parent); - - this.panelview = this._register(new PanelView(parent, this.options)); - this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel))); - this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this.panelview = this._register(new PanelView(parent, this.options)); + this._register(this.panelview.onDidDrop(({ from, to }) => this.movePanel(from as ViewletPanel, to as ViewletPanel))); + this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); + }); } private showContextMenu(event: StandardMouseEvent): void { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 5ccc6b4542f..3370c6f5bbb 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/views'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IViewsService, ViewsRegistry, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID } from 'vs/workbench/common/views'; +import { IViewsService, ViewsRegistry, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IView } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -173,11 +173,6 @@ class ViewDescriptorCollection extends Disposable { } } -export interface IView { - viewDescriptor: IViewDescriptor; - visible: boolean; -} - export interface IViewState { visible: boolean; collapsed: boolean; @@ -321,26 +316,17 @@ export class ContributableViewsModel extends Disposable { } private compareViewDescriptors(a: IViewDescriptor, b: IViewDescriptor): number { - const viewStateA = this.viewStates.get(a.id); - const viewStateB = this.viewStates.get(b.id); - - let orderA = viewStateA && viewStateA.order; - orderA = typeof orderA === 'number' ? orderA : a.order; - orderA = typeof orderA === 'number' ? orderA : Number.POSITIVE_INFINITY; - - let orderB = viewStateB && viewStateB.order; - orderB = typeof orderB === 'number' ? orderB : b.order; - orderB = typeof orderB === 'number' ? orderB : Number.POSITIVE_INFINITY; - - if (orderA !== orderB) { - return orderA - orderB; - } - if (a.id === b.id) { return 0; } - return a.id < b.id ? -1 : 1; + return (this.getViewOrder(a) - this.getViewOrder(b)) || (a.id < b.id ? -1 : 1); + } + + private getViewOrder(viewDescriptor: IViewDescriptor): number { + const viewState = this.viewStates.get(viewDescriptor.id); + const viewOrder = viewState && typeof viewState.order === 'number' ? viewState.order : viewDescriptor.order; + return typeof viewOrder === 'number' ? viewOrder : Number.MAX_VALUE; } private onDidChangeViewDescriptors(viewDescriptors: IViewDescriptor[]): void { @@ -355,13 +341,12 @@ export class ContributableViewsModel extends Disposable { for (const viewDescriptor of viewDescriptors) { const viewState = this.viewStates.get(viewDescriptor.id); if (viewState) { - if (isUndefinedOrNull(viewState.collapsed)) { - // collapsed state was not set, so set it from view descriptor - viewState.collapsed = !!viewDescriptor.collapsed; - } + // set defaults if not set + viewState.visible = isUndefinedOrNull(viewState.visible) ? !viewDescriptor.hideByDefault : viewState.visible; + viewState.collapsed = isUndefinedOrNull(viewState.collapsed) ? !!viewDescriptor.collapsed : viewState.collapsed; } else { this.viewStates.set(viewDescriptor.id, { - visible: true, + visible: !viewDescriptor.hideByDefault, collapsed: viewDescriptor.collapsed }); } @@ -436,8 +421,8 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { this.storageService = storageService; this.contextService = contextService; - this._register(this.onDidAdd(() => this.saveVisibilityStates())); - this._register(this.onDidRemove(() => this.saveVisibilityStates())); + this._register(this.onDidAdd(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); + this._register(this.onDidRemove(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); } saveViewsStates(): void { @@ -451,9 +436,9 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { this.storageService.store(this.viewletStateStorageId, JSON.stringify(storedViewsStates), this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL); } - private saveVisibilityStates(): void { - const storedViewsVisibilityStates: { id: string, isHidden: boolean }[] = []; - for (const viewDescriptor of this.viewDescriptors) { + private saveVisibilityStates(viewDescriptors: IViewDescriptor[]): void { + const storedViewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(this.hiddenViewsStorageId, this.storageService, this.contextService); + for (const viewDescriptor of viewDescriptors) { if (viewDescriptor.canToggleVisibility) { const viewState = this.viewStates.get(viewDescriptor.id); storedViewsVisibilityStates.push({ id: viewDescriptor.id, isHidden: viewState ? !viewState.visible : void 0 }); @@ -465,8 +450,7 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { private static loadViewsStates(viewletStateStorageId: string, hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): Map { const viewStates = new Map(); const storedViewsStates = JSON.parse(storageService.get(viewletStateStorageId, contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? StorageScope.WORKSPACE : StorageScope.GLOBAL, '{}')); - const storedVisibilityStates = >JSON.parse(storageService.get(hiddenViewsStorageId, StorageScope.GLOBAL, '[]')); - const viewsVisibilityStates = <{ id: string, isHidden: boolean }[]>storedVisibilityStates.map(c => typeof c === 'string' /* migration */ ? { id: c, isHidden: true } : c); + const viewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(hiddenViewsStorageId, storageService, contextService); for (const { id, isHidden } of viewsVisibilityStates) { const viewState = storedViewsStates[id]; if (viewState) { @@ -478,12 +462,17 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { } for (const id of Object.keys(storedViewsStates)) { if (!viewStates.has(id)) { - viewStates.set(id, { ...storedViewsStates[id], ...{ visible: true } }); + viewStates.set(id, { ...storedViewsStates[id] }); } } return viewStates; } + private static loadViewsVisibilityState(hiddenViewsStorageId: string, storageService: IStorageService, contextService: IWorkspaceContextService): { id: string, isHidden: boolean }[] { + const storedVisibilityStates = >JSON.parse(storageService.get(hiddenViewsStorageId, StorageScope.GLOBAL, '[]')); + return <{ id: string, isHidden: boolean }[]>storedVisibilityStates.map(c => typeof c === 'string' /* migration */ ? { id: c, isHidden: true } : c); + } + dispose(): void { this.saveViewsStates(); super.dispose(); @@ -500,17 +489,18 @@ export class ViewsService extends Disposable implements IViewsService { @IInstantiationService private instantiationService: IInstantiationService, @ILifecycleService private lifecycleService: ILifecycleService, @IViewletService private viewletService: IViewletService, - @IStorageService private storageService: IStorageService + @IStorageService private storageService: IStorageService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService ) { super(); const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); viewContainersRegistry.all.forEach(viewContainer => this.onDidRegisterViewContainer(viewContainer)); this._register(viewContainersRegistry.onDidRegister(viewContainer => this.onDidRegisterViewContainer(viewContainer))); - this._register(Registry.as(ViewletExtensions.Viewlets).onDidRegister(viewlet => this.viewletService.setViewletEnablement(viewlet.id, this.storageService.getBoolean(`viewservice.${viewlet.id}.enablement`, StorageScope.GLOBAL, viewlet.id !== TEST_VIEW_CONTAINER_ID)))); + this._register(Registry.as(ViewletExtensions.Viewlets).onDidRegister(viewlet => this.viewletService.setViewletEnablement(viewlet.id, this.storageService.getBoolean(`viewservice.${viewlet.id}.enablement`, this.getStorageScope(), viewlet.id !== TEST_VIEW_CONTAINER_ID)))); } - openView(id: string, focus: boolean): TPromise { + openView(id: string, focus: boolean): TPromise { const viewDescriptor = ViewsRegistry.getView(id); if (viewDescriptor) { const viewletDescriptor = this.viewletService.getViewlet(viewDescriptor.container.id); @@ -539,6 +529,10 @@ export class ViewsService extends Disposable implements IViewsService { private updateViewletEnablement(viewContainer: ViewContainer, viewDescriptorCollection: ViewDescriptorCollection): void { const enabled = viewDescriptorCollection.viewDescriptors.length > 0; this.viewletService.setViewletEnablement(viewContainer.id, enabled); - this.storageService.store(`viewservice.${viewContainer.id}.enablement`, enabled, StorageScope.GLOBAL); + this.storageService.store(`viewservice.${viewContainer.id}.enablement`, enabled, this.getStorageScope()); + } + + private getStorageScope(): StorageScope { + return this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY ? StorageScope.GLOBAL : StorageScope.WORKSPACE; } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 9764a44dc74..b2544d75ffe 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import * as errors from 'vs/base/common/errors'; import * as DOM from 'vs/base/browser/dom'; import { Scope } from 'vs/workbench/common/memento'; import { dispose, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,7 +12,7 @@ import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { firstIndex } from 'vs/base/common/arrays'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IViewDescriptor, IViewsViewlet, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewsViewlet, IViewContainersRegistry, Extensions as ViewContainerExtensions, IView } from 'vs/workbench/common/views'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -89,7 +88,7 @@ export abstract class TreeViewsViewletPanel extends ViewletPanel { // Make sure the current selected element is revealed const selectedElement = this.tree.getSelection()[0]; if (selectedElement) { - this.tree.reveal(selectedElement, 0.5).done(null, errors.onUnexpectedError); + this.tree.reveal(selectedElement, 0.5); } // Pass Focus to Viewer @@ -144,31 +143,31 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - this._register(this.onDidSashChange(() => this.saveViewSizes())); - this.viewsModel.onDidAdd(added => this.onDidAddViews(added)); - this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed)); - const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => { - const size = this.viewsModel.getSize(viewDescriptor.id); - const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id); - return ({ viewDescriptor, index, size, collapsed }); - }); - if (addedViews.length) { - this.onDidAddViews(addedViews); - } - - // Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609 - this.extensionService.whenInstalledExtensionsRegistered().then(() => { - this.areExtensionsReady = true; - if (this.panels.length) { - this.updateTitleArea(); - this.updateViewHeaders(); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this._register(this.onDidSashChange(() => this.saveViewSizes())); + this.viewsModel.onDidAdd(added => this.onDidAddViews(added)); + this.viewsModel.onDidRemove(removed => this.onDidRemoveViews(removed)); + const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => { + const size = this.viewsModel.getSize(viewDescriptor.id); + const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id); + return ({ viewDescriptor, index, size, collapsed }); + }); + if (addedViews.length) { + this.onDidAddViews(addedViews); } - }); - this.focus(); + // Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609 + this.extensionService.whenInstalledExtensionsRegistered().then(() => { + this.areExtensionsReady = true; + if (this.panels.length) { + this.updateTitleArea(); + this.updateViewHeaders(); + } + }); + + this.focus(); + }); } getContextMenuActions(): IAction[] { @@ -198,7 +197,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView .then(() => void 0); } - openView(id: string, focus?: boolean): TPromise { + openView(id: string, focus?: boolean): TPromise { if (focus) { this.focus(); } @@ -207,11 +206,11 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView this.toggleViewVisibility(id); } view = this.getView(id); - if (view) { - view.setExpanded(true); + view.setExpanded(true); + if (focus) { view.focus(); } - return TPromise.as(null); + return TPromise.as(view); } movePanel(from: ViewletPanel, to: ViewletPanel): void { @@ -334,8 +333,16 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView }); } - private toggleViewVisibility(id: string): void { - this.viewsModel.setVisible(id, !this.viewsModel.isVisible(id)); + private toggleViewVisibility(viewId: string): void { + const visible = !this.viewsModel.isVisible(viewId); + /* __GDPR__ + "views.toggleVisibility" : { + "viewId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "visible": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('views.toggledVisibility', { viewId, visible }); + this.viewsModel.setVisible(viewId, visible); } private saveViewSizes(): void { @@ -369,7 +376,7 @@ export abstract class ViewContainerViewlet extends PanelViewlet implements IView private computeInitialSizes(): { [id: string]: number } { let sizes = {}; if (this.dimension) { - let totalWeight = 0; + const totalWeight = this.viewsModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0); for (const viewDescriptor of this.viewsModel.visibleViewDescriptors) { sizes[viewDescriptor.id] = this.dimension.height * (viewDescriptor.weight || 20) / totalWeight; } diff --git a/src/vs/workbench/browser/quickopen.ts b/src/vs/workbench/browser/quickopen.ts index 407acc7b5d5..3675ccf763e 100644 --- a/src/vs/workbench/browser/quickopen.ts +++ b/src/vs/workbench/browser/quickopen.ts @@ -19,6 +19,7 @@ import { IResourceInput, IEditorOptions } from 'vs/platform/editor/common/editor import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const CLOSE_ON_FOCUS_LOST_CONFIG = 'workbench.quickOpen.closeOnFocusLost'; @@ -42,21 +43,21 @@ export class QuickOpenHandler { * As such, returning the same model instance across multiple searches will yield best * results in terms of performance when many items are shown. */ - public getResults(searchValue: string): TPromise> { + getResults(searchValue: string, token: CancellationToken): TPromise> { return TPromise.as(null); } /** * The ARIA label to apply when this quick open handler is active in quick open. */ - public getAriaLabel(): string { + getAriaLabel(): string { return null; } /** * Extra CSS class name to add to the quick open widget to do custom styling of entries. */ - public getClass(): string { + getClass(): string { return null; } @@ -64,14 +65,14 @@ export class QuickOpenHandler { * Indicates if the handler can run in the current environment. Return a string if the handler cannot run but has * a good message to show in this case. */ - public canRun(): boolean | string { + canRun(): boolean | string { return true; } /** * Hints to the outside that this quick open handler typically returns results fast. */ - public hasShortResponseTime(): boolean { + hasShortResponseTime(): boolean { return false; } @@ -79,14 +80,14 @@ export class QuickOpenHandler { * Indicates if the handler wishes the quick open widget to automatically select the first result entry or an entry * based on a specific prefix match. */ - public getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { + getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { return {}; } /** * Indicates to the handler that the quick open widget has been opened. */ - public onOpen(): void { + onOpen(): void { return; } @@ -94,21 +95,21 @@ export class QuickOpenHandler { * Indicates to the handler that the quick open widget has been closed. Allows to free up any resources as needed. * The parameter canceled indicates if the quick open widget was closed with an entry being run or not. */ - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { return; } /** * Allows to return a label that will be placed to the side of the results from this handler or null if none. */ - public getGroupLabel(): string { + getGroupLabel(): string { return null; } /** * Allows to return a label that will be used when there are no results found */ - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { if (searchString.length > 0) { return nls.localize('noResultsMatching', "No results matching"); } @@ -126,11 +127,11 @@ export interface QuickOpenHandlerHelpEntry { * A lightweight descriptor of a quick open handler. */ export class QuickOpenHandlerDescriptor { - public prefix: string; - public description: string; - public contextKey: string; - public helpEntries: QuickOpenHandlerHelpEntry[]; - public instantProgress: boolean; + prefix: string; + description: string; + contextKey: string; + helpEntries: QuickOpenHandlerHelpEntry[]; + instantProgress: boolean; private id: string; private ctor: IConstructorSignature0; @@ -151,11 +152,11 @@ export class QuickOpenHandlerDescriptor { } } - public getId(): string { + getId(): string { return this.id; } - public instantiate(instantiationService: IInstantiationService): QuickOpenHandler { + instantiate(instantiationService: IInstantiationService): QuickOpenHandler { return instantiationService.createInstance(this.ctor); } } @@ -193,14 +194,10 @@ export interface IQuickOpenRegistry { } class QuickOpenRegistry implements IQuickOpenRegistry { - private handlers: QuickOpenHandlerDescriptor[]; + private handlers: QuickOpenHandlerDescriptor[] = []; private defaultHandler: QuickOpenHandlerDescriptor; - constructor() { - this.handlers = []; - } - - public registerQuickOpenHandler(descriptor: QuickOpenHandlerDescriptor): void { + registerQuickOpenHandler(descriptor: QuickOpenHandlerDescriptor): void { this.handlers.push(descriptor); // sort the handlers by decreasing prefix length, such that longer @@ -208,19 +205,19 @@ class QuickOpenRegistry implements IQuickOpenRegistry { this.handlers.sort((h1, h2) => h2.prefix.length - h1.prefix.length); } - public registerDefaultQuickOpenHandler(descriptor: QuickOpenHandlerDescriptor): void { + registerDefaultQuickOpenHandler(descriptor: QuickOpenHandlerDescriptor): void { this.defaultHandler = descriptor; } - public getQuickOpenHandlers(): QuickOpenHandlerDescriptor[] { + getQuickOpenHandlers(): QuickOpenHandlerDescriptor[] { return this.handlers.slice(0); } - public getQuickOpenHandler(text: string): QuickOpenHandlerDescriptor { + getQuickOpenHandler(text: string): QuickOpenHandlerDescriptor { return text ? arrays.first(this.handlers, h => strings.startsWith(text, h.prefix), null) : null; } - public getDefaultQuickOpenHandler(): QuickOpenHandlerDescriptor { + getDefaultQuickOpenHandler(): QuickOpenHandlerDescriptor { return this.defaultHandler; } } @@ -249,19 +246,19 @@ export class EditorQuickOpenEntry extends QuickOpenEntry implements IEditorQuick super(); } - public get editorService() { + get editorService() { return this._editorService; } - public getInput(): IResourceInput | IEditorInput { + getInput(): IResourceInput | IEditorInput { return null; } - public getOptions(): IEditorOptions { + getOptions(): IEditorOptions { return null; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { const hideWidget = (mode === Mode.OPEN); if (mode === Mode.OPEN || mode === Mode.OPEN_IN_BACKGROUND) { @@ -304,11 +301,11 @@ export class EditorQuickOpenEntry extends QuickOpenEntry implements IEditorQuick */ export class EditorQuickOpenEntryGroup extends QuickOpenEntryGroup implements IEditorQuickOpenEntry { - public getInput(): IEditorInput | IResourceInput { + getInput(): IEditorInput | IResourceInput { return null; } - public getOptions(): IEditorOptions { + getOptions(): IEditorOptions { return null; } } @@ -328,7 +325,7 @@ export class QuickOpenAction extends Action { this.enabled = !!this.quickOpenService; } - public run(context?: any): TPromise { + run(context?: any): TPromise { // Show with prefix this.quickOpenService.show(this.prefix); diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index b67b491a4b1..5d0bc64e5e8 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -18,6 +18,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPartService } 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'; export abstract class Viewlet extends Composite implements IViewlet { @@ -29,11 +30,11 @@ export abstract class Viewlet extends Composite implements IViewlet { super(id, telemetryService, themeService); } - public getOptimalWidth(): number { + getOptimalWidth(): number { return null; } - public getContextMenuActions(): IAction[] { + getContextMenuActions(): IAction[] { return [{ id: ToggleSidebarVisibilityAction.ID, label: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), @@ -54,12 +55,12 @@ export class ViewletDescriptor extends CompositeDescriptor { name: string, cssClass?: string, order?: number, - private _iconUrl?: string + private _iconUrl?: URI ) { super(ctor, id, name, cssClass, order, id); } - public get iconUrl(): string { + get iconUrl(): URI { return this._iconUrl; } } @@ -74,35 +75,35 @@ export class ViewletRegistry extends CompositeRegistry { /** * Registers a viewlet to the platform. */ - public registerViewlet(descriptor: ViewletDescriptor): void { + registerViewlet(descriptor: ViewletDescriptor): void { super.registerComposite(descriptor); } /** * Returns the viewlet descriptor for the given id or null if none. */ - public getViewlet(id: string): ViewletDescriptor { + getViewlet(id: string): ViewletDescriptor { return this.getComposite(id) as ViewletDescriptor; } /** * Returns an array of registered viewlets known to the platform. */ - public getViewlets(): ViewletDescriptor[] { + getViewlets(): ViewletDescriptor[] { return this.getComposites() as ViewletDescriptor[]; } /** * Sets the id of the viewlet that should open on startup by default. */ - public setDefaultViewletId(id: string): void { + setDefaultViewletId(id: string): void { this.defaultViewletId = id; } /** * Gets the id of the viewlet that should open on startup by default. */ - public getDefaultViewletId(): string { + getDefaultViewletId(): string { return this.defaultViewletId; } } @@ -128,7 +129,7 @@ export class ToggleViewletAction extends Action { this.enabled = !!this.viewletService && !!this.editorGroupService; } - public run(): TPromise { + run(): TPromise { // Pass focus to viewlet if not open or focused if (this.otherViewletShowing() || !this.sidebarHasFocus()) { diff --git a/src/vs/workbench/buildfile.js b/src/vs/workbench/buildfile.js index 4b83a39ab10..1dfb66c06b4 100644 --- a/src/vs/workbench/buildfile.js +++ b/src/vs/workbench/buildfile.js @@ -27,8 +27,6 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []), createModuleDescription('vs/workbench/node/extensionHostProcess', []), - - createModuleDescription('vs/workbench/parts/terminal/node/terminalProcess', []) ]; return modules; diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index a2533c808d4..723636de8bc 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -6,13 +6,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; export const Extensions = { WorkbenchActions: 'workbench.contributions.actions' @@ -24,29 +25,28 @@ export interface IWorkbenchActionRegistry { * Registers a workbench action to the platform. Workbench actions are not * visible by default and can only be invoked through a keybinding if provided. */ - registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string): IDisposable; + registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable; } Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionRegistry { - registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string): IDisposable { - return this._registerWorkbenchCommandFromAction(descriptor, alias, category); + registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable { + return this._registerWorkbenchCommandFromAction(descriptor, alias, category, when); } - private _registerWorkbenchCommandFromAction(descriptor: SyncActionDescriptor, alias: string, category?: string): IDisposable { + private _registerWorkbenchCommandFromAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable { let registrations: IDisposable[] = []; // command registrations.push(CommandsRegistry.registerCommand(descriptor.id, this._createCommandHandler(descriptor))); // keybinding - const when = descriptor.keybindingContext; - const weight = (typeof descriptor.keybindingWeight === 'undefined' ? KeybindingsRegistry.WEIGHT.workbenchContrib() : descriptor.keybindingWeight); + const weight = (typeof descriptor.keybindingWeight === 'undefined' ? KeybindingWeight.WorkbenchContrib : descriptor.keybindingWeight); const keybindings = descriptor.keybindings; KeybindingsRegistry.registerKeybindingRule({ id: descriptor.id, weight: weight, - when: when, + when: descriptor.keybindingContext, primary: keybindings && keybindings.primary, secondary: keybindings && keybindings.secondary, win: keybindings && keybindings.win, @@ -74,7 +74,7 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR MenuRegistry.addCommand(command); - registrations.push(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command })); + registrations.push(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when })); } // TODO@alex,joh diff --git a/src/vs/workbench/common/component.ts b/src/vs/workbench/common/component.ts index 99cafcca30c..f855f0a8a30 100644 --- a/src/vs/workbench/common/component.ts +++ b/src/vs/workbench/common/component.ts @@ -45,7 +45,7 @@ export class Component extends Themable implements IWorkbenchComponent { this.componentMemento = new Memento(this.id); } - public getId(): string { + getId(): string { return this.id; } @@ -73,7 +73,7 @@ export class Component extends Themable implements IWorkbenchComponent { this.componentMemento.saveMemento(); } - public shutdown(): void { + shutdown(): void { // Save Memento this.saveMemento(); diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index fb6612f3a7c..46d05c51c77 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -6,7 +6,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService, IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase, LifecyclePhaseToString } from 'vs/platform/lifecycle/common/lifecycle'; +import { mark } from 'vs/base/common/performance'; // --- Workbench Contribution Registry @@ -45,7 +46,7 @@ export class WorkbenchContributionsRegistry implements IWorkbenchContributionsRe private toBeInstantiated: Map[]> = new Map[]>(); - public registerWorkbenchContribution(ctor: IWorkbenchContributionSignature, phase: LifecyclePhase = LifecyclePhase.Starting): void { + registerWorkbenchContribution(ctor: IWorkbenchContributionSignature, phase: LifecyclePhase = LifecyclePhase.Starting): void { // Instantiate directly if we are already matching the provided phase if (this.instantiationService && this.lifecycleService && this.lifecycleService.phase >= phase) { @@ -64,7 +65,7 @@ export class WorkbenchContributionsRegistry implements IWorkbenchContributionsRe } } - public start(instantiationService: IInstantiationService, lifecycleService: ILifecycleService): void { + start(instantiationService: IInstantiationService, lifecycleService: ILifecycleService): void { this.instantiationService = instantiationService; this.lifecycleService = lifecycleService; @@ -89,13 +90,16 @@ export class WorkbenchContributionsRegistry implements IWorkbenchContributionsRe } private doInstantiateByPhase(instantiationService: IInstantiationService, phase: LifecyclePhase): void { + mark(`LifecyclePhase/${LifecyclePhaseToString(phase)}/createContrib:start`); const toBeInstantiated = this.toBeInstantiated.get(phase); if (toBeInstantiated) { while (toBeInstantiated.length > 0) { - instantiationService.createInstance(toBeInstantiated.shift()); + const ctor = toBeInstantiated.shift(); + instantiationService.createInstance(ctor); } } + mark(`LifecyclePhase/${LifecyclePhaseToString(phase)}/createContrib:end`); } } -Registry.add(Extensions.Workbench, new WorkbenchContributionsRegistry()); \ No newline at end of file +Registry.add(Extensions.Workbench, new WorkbenchContributionsRegistry()); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 9a0e6fe7579..0ec4e750428 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -8,8 +8,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter, once } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; import * as types from 'vs/base/common/types'; -import URI from 'vs/base/common/uri'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; @@ -17,15 +17,16 @@ import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/con import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; -import { LRUCache } from 'vs/base/common/map'; -import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { ICompositeControl } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; +export const ActiveEditorContext = new RawContextKey('activeEditor', null); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); export const EditorGroupActiveEditorDirtyContext = new RawContextKey('groupActiveEditorDirty', false); export const NoEditorsVisibleContext: ContextKeyExpr = EditorsVisibleContext.toNegated(); export const TextCompareEditorVisibleContext = new RawContextKey('textCompareEditorVisible', false); +export const TextCompareEditorActiveContext = new RawContextKey('textCompareEditorActive', false); export const ActiveEditorGroupEmptyContext = new RawContextKey('activeEditorGroupEmpty', false); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false); export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); @@ -59,6 +60,31 @@ export interface IEditor { */ group: IEditorGroup; + /** + * The minimum width of this editor. + */ + readonly minimumWidth: number; + + /** + * The maximum width of this editor. + */ + readonly maximumWidth: number; + + /** + * The minimum height of this editor. + */ + readonly minimumHeight: number; + + /** + * The maximum height of this editor. + */ + readonly maximumHeight: number; + + /** + * An event to notify whenever minimum/maximum width/height changes. + */ + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }>; + /** * Returns the unique identifier of this editor. */ @@ -223,7 +249,7 @@ export interface IResourceSideBySideInput extends IBaseResourceInput { detailResource: URI; } -export enum Verbosity { +export const enum Verbosity { SHORT, MEDIUM, LONG @@ -254,6 +280,11 @@ export interface IEditorInput extends IDisposable { */ getResource(): URI; + /** + * Unique type identifier for this inpput. + */ + getTypeId(): string; + /** * Returns the display name of this input. */ @@ -294,46 +325,28 @@ export interface IEditorInput extends IDisposable { * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. */ -export abstract class EditorInput implements IEditorInput { - private readonly _onDispose: Emitter; - protected _onDidChangeDirty: Emitter; - protected _onDidChangeLabel: Emitter; +export abstract class EditorInput extends Disposable implements IEditorInput { - private disposed: boolean; + protected readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } - constructor() { - this._onDidChangeDirty = new Emitter(); - this._onDidChangeLabel = new Emitter(); - this._onDispose = new Emitter(); + protected readonly _onDidChangeLabel: Emitter = this._register(new Emitter()); + get onDidChangeLabel(): Event { return this._onDidChangeLabel.event; } - this.disposed = false; - } + private readonly _onDispose: Emitter = this._register(new Emitter()); + get onDispose(): Event { return this._onDispose.event; } + + private disposed: boolean = false; /** - * Fired when the dirty state of this input changes. + * Returns the unique type identifier of this input. */ - public get onDidChangeDirty(): Event { - return this._onDidChangeDirty.event; - } - - /** - * Fired when the label this input changes. - */ - public get onDidChangeLabel(): Event { - return this._onDidChangeLabel.event; - } - - /** - * Fired when the model gets disposed. - */ - public get onDispose(): Event { - return this._onDispose.event; - } + abstract getTypeId(): string; /** * Returns the associated resource of this input if any. */ - public getResource(): URI { + getResource(): URI { return null; } @@ -341,7 +354,7 @@ export abstract class EditorInput implements IEditorInput { * Returns the name of this input that can be shown to the user. Examples include showing the name of the input * above the editor area when the input is shown. */ - public getName(): string { + getName(): string { return null; } @@ -349,24 +362,23 @@ export abstract class EditorInput implements IEditorInput { * Returns the description of this input that can be shown to the user. Examples include showing the description of * the input above the editor area to the side of the name of the input. */ - public getDescription(verbosity?: Verbosity): string { + getDescription(verbosity?: Verbosity): string { return null; } - public getTitle(verbosity?: Verbosity): string { + /** + * Returns the title of this input that can be shown to the user. Examples include showing the title of + * the input above the editor area as hover over the input label. + */ + getTitle(verbosity?: Verbosity): string { return this.getName(); } - /** - * Returns the unique type identifier of this input. - */ - public abstract getTypeId(): string; - /** * Returns the preferred editor for this input. A list of candidate editors is passed in that whee registered * for the input. This allows subclasses to decide late which editor to use for the input on a case by case basis. */ - public getPreferredEditorId(candidates: string[]): string { + getPreferredEditorId(candidates: string[]): string { if (candidates && candidates.length > 0) { return candidates[0]; } @@ -379,7 +391,7 @@ export abstract class EditorInput implements IEditorInput { * * Subclasses should extend if they can contribute. */ - public getTelemetryDescriptor(): object { + getTelemetryDescriptor(): object { /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } @@ -390,89 +402,85 @@ export abstract class EditorInput implements IEditorInput { /** * Returns a type of EditorModel that represents the resolved input. Subclasses should - * override to provide a meaningful model. The optional second argument allows to specify - * if the EditorModel should be refreshed before returning it. Depending on the implementation - * this could mean to refresh the editor model contents with the version from disk. + * override to provide a meaningful model. */ - public abstract resolve(refresh?: boolean): TPromise; + abstract resolve(): TPromise; /** * An editor that is dirty will be asked to be saved once it closes. */ - public isDirty(): boolean { + isDirty(): boolean { return false; } /** * Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result. */ - public confirmSave(): TPromise { + confirmSave(): TPromise { return TPromise.wrap(ConfirmResult.DONT_SAVE); } /** * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ - public save(): TPromise { + save(): TPromise { return TPromise.as(true); } /** * Reverts the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ - public revert(options?: IRevertOptions): TPromise { + revert(options?: IRevertOptions): TPromise { return TPromise.as(true); } /** * Called when this input is no longer opened in any editor. Subclasses can free resources as needed. */ - public close(): void { + close(): void { this.dispose(); } /** * Subclasses can set this to false if it does not make sense to split the editor input. */ - public supportsSplitEditor(): boolean { + supportsSplitEditor(): boolean { return true; } /** * Returns true if this input is identical to the otherInput. */ - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { return this === otherInput; } + /** + * Returns whether this input was disposed or not. + */ + isDisposed(): boolean { + return this.disposed; + } + /** * Called when an editor input is no longer needed. Allows to free up any resources taken by * resolving the editor input. */ - public dispose(): void { + dispose(): void { this.disposed = true; this._onDispose.fire(); - this._onDidChangeDirty.dispose(); - this._onDidChangeLabel.dispose(); - this._onDispose.dispose(); - } - - /** - * Returns whether this input was disposed or not. - */ - public isDisposed(): boolean { - return this.disposed; + super.dispose(); } } -export enum ConfirmResult { +export const enum ConfirmResult { SAVE, DONT_SAVE, CANCEL } -export enum EncodingMode { +export const enum EncodingMode { /** * Instructs the encoding support to encode the current input with the provided encoding @@ -520,13 +528,11 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport { */ export class SideBySideEditorInput extends EditorInput { - public static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; - - private _toUnbind: IDisposable[]; + static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; constructor(private name: string, private description: string, private _details: EditorInput, private _master: EditorInput) { super(); - this._toUnbind = []; + this.registerListeners(); } @@ -538,23 +544,23 @@ export class SideBySideEditorInput extends EditorInput { return this._details; } - public isDirty(): boolean { + isDirty(): boolean { return this.master.isDirty(); } - public confirmSave(): TPromise { + confirmSave(): TPromise { return this.master.confirmSave(); } - public save(): TPromise { + save(): TPromise { return this.master.save(); } - public revert(): TPromise { + revert(): TPromise { return this.master.revert(); } - public getTelemetryDescriptor(): object { + getTelemetryDescriptor(): object { const descriptor = this.master.getTelemetryDescriptor(); return objects.assign(descriptor, super.getTelemetryDescriptor()); } @@ -563,29 +569,25 @@ export class SideBySideEditorInput extends EditorInput { // When the details or master input gets disposed, dispose this diff editor input const onceDetailsDisposed = once(this.details.onDispose); - this._toUnbind.push(onceDetailsDisposed(() => { + this._register(onceDetailsDisposed(() => { if (!this.isDisposed()) { this.dispose(); } })); const onceMasterDisposed = once(this.master.onDispose); - this._toUnbind.push(onceMasterDisposed(() => { + this._register(onceMasterDisposed(() => { if (!this.isDisposed()) { this.dispose(); } })); // Reemit some events from the master side to the outside - this._toUnbind.push(this.master.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._toUnbind.push(this.master.onDidChangeLabel(() => this._onDidChangeLabel.fire())); + this._register(this.master.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.master.onDidChangeLabel(() => this._onDidChangeLabel.fire())); } - public get toUnbind() { - return this._toUnbind; - } - - public resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } @@ -593,19 +595,15 @@ export class SideBySideEditorInput extends EditorInput { return SideBySideEditorInput.ID; } - public getName(): string { + getName(): string { return this.name; } - public getDescription(): string { + getDescription(): string { return this.description; } - public supportsSplitEditor(): boolean { - return false; - } - - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } @@ -621,11 +619,6 @@ export class SideBySideEditorInput extends EditorInput { return false; } - - public dispose(): void { - this._toUnbind = dispose(this._toUnbind); - super.dispose(); - } } export interface ITextEditorModel extends IEditorModel { @@ -638,40 +631,30 @@ export interface ITextEditorModel extends IEditorModel { * are typically cached for some while because they are expensive to construct. */ export class EditorModel extends Disposable implements IEditorModel { - private readonly _onDispose: Emitter; - constructor() { - super(); - this._onDispose = new Emitter(); - } - - /** - * Fired when the model gets disposed. - */ - public get onDispose(): Event { - return this._onDispose.event; - } + private readonly _onDispose: Emitter = this._register(new Emitter()); + get onDispose(): Event { return this._onDispose.event; } /** * Causes this model to load returning a promise when loading is completed. */ - public load(): TPromise { + load(): TPromise { return TPromise.as(this); } /** * Returns whether this model was loaded or not. */ - public isResolved(): boolean { + isResolved(): boolean { return true; } /** * Subclasses should implement to free resources that have been claimed through loading. */ - public dispose(): void { + dispose(): void { this._onDispose.fire(); - this._onDispose.dispose(); + super.dispose(); } } @@ -695,11 +678,11 @@ export class EditorOptions implements IEditorOptions { /** * Helper to create EditorOptions inline. */ - public static create(settings: IEditorOptions): EditorOptions { + static create(settings: IEditorOptions): EditorOptions { const options = new EditorOptions(); options.preserveFocus = settings.preserveFocus; - options.forceOpen = settings.forceOpen; + options.forceReload = settings.forceReload; options.revealIfVisible = settings.revealIfVisible; options.revealIfOpened = settings.revealIfOpened; options.pinned = settings.pinned; @@ -713,41 +696,41 @@ export class EditorOptions implements IEditorOptions { * Tells the editor to not receive keyboard focus when the editor is being opened. By default, * the editor will receive keyboard focus on open. */ - public preserveFocus: boolean; + preserveFocus: boolean; /** - * Tells the editor to replace the editor input in the editor even if it is identical to the one - * already showing. By default, the editor will not replace the input if it is identical to the + * Tells the editor to reload the editor input in the editor even if it is identical to the one + * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ - public forceOpen: boolean; + forceReload: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. */ - public revealIfVisible: boolean; + revealIfVisible: boolean; /** * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. */ - public revealIfOpened: boolean; + revealIfOpened: boolean; /** * An editor that is pinned remains in the editor stack even when another editor is being opened. * An editor that is not pinned will always get replaced by another editor that is not pinned. */ - public pinned: boolean; + pinned: boolean; /** * The index in the document stack where to insert the editor into when opening. */ - public index: number; + index: number; /** * An active editor that is opened will show its contents directly. Set to true to open an editor * in the background. */ - public inactive: boolean; + inactive: boolean; } /** @@ -762,7 +745,7 @@ export class TextEditorOptions extends EditorOptions { private revealInCenterIfOutsideViewport: boolean; private editorViewState: IEditorViewState; - public static from(input?: IBaseResourceInput): TextEditorOptions { + static from(input?: IBaseResourceInput): TextEditorOptions { if (!input || !input.options) { return null; } @@ -773,7 +756,7 @@ export class TextEditorOptions extends EditorOptions { /** * Helper to convert options bag to real class */ - public static create(options: ITextEditorOptions = Object.create(null)): TextEditorOptions { + static create(options: ITextEditorOptions = Object.create(null)): TextEditorOptions { const textEditorOptions = new TextEditorOptions(); if (options.selection) { @@ -785,8 +768,8 @@ export class TextEditorOptions extends EditorOptions { textEditorOptions.editorViewState = options.viewState as IEditorViewState; } - if (options.forceOpen) { - textEditorOptions.forceOpen = true; + if (options.forceReload) { + textEditorOptions.forceReload = true; } if (options.revealIfVisible) { @@ -823,14 +806,14 @@ export class TextEditorOptions extends EditorOptions { /** * Returns if this options object has objects defined for the editor. */ - public hasOptionsDefined(): boolean { + hasOptionsDefined(): boolean { return !!this.editorViewState || (!types.isUndefinedOrNull(this.startLineNumber) && !types.isUndefinedOrNull(this.startColumn)); } /** * Tells the editor to set show the given selection when the editor is being opened. */ - public selection(startLineNumber: number, startColumn: number, endLineNumber: number = startLineNumber, endColumn: number = startColumn): EditorOptions { + selection(startLineNumber: number, startColumn: number, endLineNumber: number = startLineNumber, endColumn: number = startColumn): EditorOptions { this.startLineNumber = startLineNumber; this.startColumn = startColumn; this.endLineNumber = endLineNumber; @@ -842,7 +825,7 @@ export class TextEditorOptions extends EditorOptions { /** * Create a TextEditorOptions inline to be used when the editor is opening. */ - public static fromEditor(editor: ICodeEditor, settings?: IEditorOptions): TextEditorOptions { + static fromEditor(editor: ICodeEditor, settings?: IEditorOptions): TextEditorOptions { const options = TextEditorOptions.create(settings); // View state @@ -856,7 +839,7 @@ export class TextEditorOptions extends EditorOptions { * * @return if something was applied */ - public apply(editor: ICodeEditor, scrollType: ScrollType): boolean { + apply(editor: ICodeEditor, scrollType: ScrollType): boolean { // View state return this.applyViewState(editor, scrollType); @@ -1014,120 +997,21 @@ export function toResource(editor: IEditorInput, options?: IResourceOptions): UR return null; } -export enum CloseDirection { +export const enum CloseDirection { LEFT, RIGHT } -interface MapGroupToViewStates { - [group: number]: T; -} +export interface IEditorMemento { -export class EditorViewStateMemento { - private cache: LRUCache>; + saveState(group: IEditorGroup, resource: URI, state: T): void; + saveState(group: IEditorGroup, editor: EditorInput, state: T): void; - constructor( - private editorGroupService: IEditorGroupsService, - private memento: object, - private key: string, - private limit: number = 10 - ) { } + loadState(group: IEditorGroup, resource: URI): T; + loadState(group: IEditorGroup, editor: EditorInput): T; - public saveState(group: IEditorGroup, resource: URI, state: T): void; - public saveState(group: IEditorGroup, editor: EditorInput, state: T): void; - public saveState(group: IEditorGroup, resourceOrEditor: URI | EditorInput, state: T): void { - const resource = this.doGetResource(resourceOrEditor); - if (!resource || !group) { - return; // we are not in a good state to save any viewstate for a resource - } - - const cache = this.doLoad(); - - let viewStates = cache.get(resource.toString()); - if (!viewStates) { - viewStates = Object.create(null) as MapGroupToViewStates; - cache.set(resource.toString(), viewStates); - } - - viewStates[group.id] = state; - - // Automatically clear when editor input gets disposed if any - if (resourceOrEditor instanceof EditorInput) { - once(resourceOrEditor.onDispose)(() => { - this.clearState(resource); - }); - } - } - - public loadState(group: IEditorGroup, resource: URI): T; - public loadState(group: IEditorGroup, editor: EditorInput): T; - public loadState(group: IEditorGroup, resourceOrEditor: URI | EditorInput): T { - const resource = this.doGetResource(resourceOrEditor); - if (!resource || !group) { - return void 0; // we are not in a good state to load any viewstate for a resource - } - - const cache = this.doLoad(); - - const viewStates = cache.get(resource.toString()); - if (viewStates) { - return viewStates[group.id]; - } - - return void 0; - } - - public clearState(resource: URI): void; - public clearState(editor: EditorInput): void; - public clearState(resourceOrEditor: URI | EditorInput): void { - const resource = this.doGetResource(resourceOrEditor); - if (resource) { - const cache = this.doLoad(); - cache.delete(resource.toString()); - } - } - - private doGetResource(resourceOrEditor: URI | EditorInput): URI { - if (resourceOrEditor instanceof EditorInput) { - return resourceOrEditor.getResource(); - } - - return resourceOrEditor; - } - - private doLoad(): LRUCache> { - if (!this.cache) { - this.cache = new LRUCache>(this.limit); - - // Restore from serialized map state - const rawViewState = this.memento[this.key]; - if (Array.isArray(rawViewState)) { - this.cache.fromJSON(rawViewState); - } - } - - return this.cache; - } - - public save(): void { - const cache = this.doLoad(); - - // Remove groups from states that no longer exist - cache.forEach((mapGroupToViewStates, resource) => { - Object.keys(mapGroupToViewStates).forEach(group => { - const groupId: GroupIdentifier = Number(group); - if (!this.editorGroupService.getGroup(groupId)) { - delete mapGroupToViewStates[groupId]; - - if (types.isEmptyObject(mapGroupToViewStates)) { - cache.delete(resource); - } - } - }); - }); - - this.memento[this.key] = cache.toJSON(); - } + clearState(resource: URI): void; + clearState(editor: EditorInput): void; } class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { @@ -1136,10 +1020,7 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private editorInputFactoryConstructors: { [editorInputId: string]: IConstructorSignature0 } = Object.create(null); private editorInputFactoryInstances: { [editorInputId: string]: IEditorInputFactory } = Object.create(null); - constructor() { - } - - public setInstantiationService(service: IInstantiationService): void { + setInstantiationService(service: IInstantiationService): void { this.instantiationService = service; for (let key in this.editorInputFactoryConstructors) { @@ -1155,15 +1036,15 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { this.editorInputFactoryInstances[editorInputId] = instance; } - public registerFileInputFactory(factory: IFileInputFactory): void { + registerFileInputFactory(factory: IFileInputFactory): void { this.fileInputFactory = factory; } - public getFileInputFactory(): IFileInputFactory { + getFileInputFactory(): IFileInputFactory { return this.fileInputFactory; } - public registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): void { + registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): void { if (!this.instantiationService) { this.editorInputFactoryConstructors[editorInputId] = ctor; } else { @@ -1171,7 +1052,7 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { } } - public getEditorInputFactory(editorInputId: string): IEditorInputFactory { + getEditorInputFactory(editorInputId: string): IEditorInputFactory { return this.editorInputFactoryInstances[editorInputId]; } } diff --git a/src/vs/workbench/common/editor/binaryEditorModel.ts b/src/vs/workbench/common/editor/binaryEditorModel.ts index d4f34881d8c..f1f425dc7a8 100644 --- a/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { EditorModel } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; import { DataUri } from 'vs/workbench/common/resources'; @@ -44,39 +44,39 @@ export class BinaryEditorModel extends EditorModel { /** * The name of the binary resource. */ - public getName(): string { + getName(): string { return this.name; } /** * The resource of the binary resource. */ - public getResource(): URI { + getResource(): URI { return this.resource; } /** * The size of the binary resource if known. */ - public getSize(): number { + getSize(): number { return this.size; } /** * The mime of the binary resource if known. */ - public getMime(): string { + getMime(): string { return this.mime; } /** * The etag of the binary resource if known. */ - public getETag(): string { + getETag(): string { return this.etag; } - public load(): TPromise { + load(): TPromise { // Make sure to resolve up to date stat for file resources if (this.fileService.canHandleResource(this.resource)) { diff --git a/src/vs/workbench/common/editor/dataUriEditorInput.ts b/src/vs/workbench/common/editor/dataUriEditorInput.ts index c1d785bd73b..77e36cf47e0 100644 --- a/src/vs/workbench/common/editor/dataUriEditorInput.ts +++ b/src/vs/workbench/common/editor/dataUriEditorInput.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { DataUri } from 'vs/workbench/common/resources'; @@ -49,27 +49,27 @@ export class DataUriEditorInput extends EditorInput { } } - public getResource(): URI { + getResource(): URI { return this.resource; } - public getTypeId(): string { + getTypeId(): string { return DataUriEditorInput.ID; } - public getName(): string { + getName(): string { return this.name; } - public getDescription(): string { + getDescription(): string { return this.description; } - public resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); } - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 7554329bd52..36f7632e566 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -16,7 +16,7 @@ import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorMo */ export class DiffEditorInput extends SideBySideEditorInput { - public static readonly ID = 'workbench.editors.diffEditorInput'; + static readonly ID = 'workbench.editors.diffEditorInput'; private cachedModel: DiffEditorModel; @@ -24,7 +24,7 @@ export class DiffEditorInput extends SideBySideEditorInput { super(name, description, original, modified); } - public getTypeId(): string { + getTypeId(): string { return DiffEditorInput.ID; } @@ -36,23 +36,13 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.master; } - public resolve(refresh?: boolean): TPromise { - let modelPromise: TPromise; - - // Use Cached Model - if (this.cachedModel && !refresh) { - modelPromise = TPromise.as(this.cachedModel); - } + resolve(): TPromise { // Create Model - we never reuse our cached model if refresh is true because we cannot // decide for the inputs within if the cached model can be reused or not. There may be // inputs that need to be loaded again and thus we always recreate the model and dispose // the previous one - if any. - else { - modelPromise = this.createModel(refresh); - } - - return modelPromise.then((resolvedModel: DiffEditorModel) => { + return this.createModel().then(resolvedModel => { if (this.cachedModel) { this.cachedModel.dispose(); } @@ -63,7 +53,7 @@ export class DiffEditorInput extends SideBySideEditorInput { }); } - public getPreferredEditorId(candidates: string[]): string { + getPreferredEditorId(candidates: string[]): string { return this.forceOpenAsBinary ? BINARY_DIFF_EDITOR_ID : TEXT_DIFF_EDITOR_ID; } @@ -71,9 +61,9 @@ export class DiffEditorInput extends SideBySideEditorInput { // Join resolve call over two inputs and build diff editor model return TPromise.join([ - this.originalInput.resolve(refresh), - this.modifiedInput.resolve(refresh) - ]).then((models) => { + this.originalInput.resolve(), + this.modifiedInput.resolve() + ]).then(models => { const originalEditorModel = models[0]; const modifiedEditorModel = models[1]; @@ -87,7 +77,7 @@ export class DiffEditorInput extends SideBySideEditorInput { }); } - public dispose(): void { + dispose(): void { // Free the diff editor model but do not propagate the dispose() call to the two inputs // We never created the two inputs (original and modified) so we can not dispose diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 47c74e1daf3..232e915b631 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -23,15 +23,15 @@ export class DiffEditorModel extends EditorModel { this._modifiedModel = modifiedModel; } - public get originalModel(): EditorModel { + get originalModel(): EditorModel { return this._originalModel as EditorModel; } - public get modifiedModel(): EditorModel { + get modifiedModel(): EditorModel { return this._modifiedModel as EditorModel; } - public load(): TPromise { + load(): TPromise { return TPromise.join([ this._originalModel.load(), this._modifiedModel.load() @@ -40,11 +40,11 @@ export class DiffEditorModel extends EditorModel { }); } - public isResolved(): boolean { + isResolved(): boolean { return this.originalModel.isResolved() && this.modifiedModel.isResolved(); } - public dispose(): void { + dispose(): void { // Do not propagate the dispose() call to the two models inside. We never created the two models // (original and modified) so we can not dispose them without sideeffects. Rather rely on the diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index ed1162c922e..d35ba11c3b7 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -7,7 +7,7 @@ import { Event, Emitter, once } from 'vs/base/common/event'; import { Extensions, IEditorInputFactoryRegistry, EditorInput, toResource, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, CloseDirection } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 0d19259d23a..0d14f8910ce 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput, ITextEditorModel } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -40,37 +40,37 @@ export class ResourceEditorInput extends EditorInput { this.resource = resource; } - public getResource(): URI { + getResource(): URI { return this.resource; } - public getTypeId(): string { + getTypeId(): string { return ResourceEditorInput.ID; } - public getName(): string { + getName(): string { return this.name; } - public setName(name: string): void { + setName(name: string): void { if (this.name !== name) { this.name = name; this._onDidChangeLabel.fire(); } } - public getDescription(): string { + getDescription(): string { return this.description; } - public setDescription(description: string): void { + setDescription(description: string): void { if (this.description !== description) { this.description = description; this._onDidChangeLabel.fire(); } } - public getTelemetryDescriptor(): object { + getTelemetryDescriptor(): object { const descriptor = super.getTelemetryDescriptor(); descriptor['resource'] = telemetryURIDescriptor(this.resource, path => this.hashService.createSHA1(path)); @@ -82,7 +82,7 @@ export class ResourceEditorInput extends EditorInput { return descriptor; } - public resolve(refresh?: boolean): TPromise { + resolve(): TPromise { if (!this.modelReference) { this.modelReference = this.textModelResolverService.createModelReference(this.resource); } @@ -101,7 +101,7 @@ export class ResourceEditorInput extends EditorInput { }); } - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } @@ -116,9 +116,9 @@ export class ResourceEditorInput extends EditorInput { return false; } - public dispose(): void { + dispose(): void { if (this.modelReference) { - this.modelReference.done(ref => ref.dispose()); + this.modelReference.then(ref => ref.dispose()); this.modelReference = null; } diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts index a195fbe3ebe..b990774ae65 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/resourceEditorModel.ts @@ -5,7 +5,7 @@ 'use strict'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -24,4 +24,8 @@ export class ResourceEditorModel extends BaseTextEditorModel { // TODO@Joao: force this class to dispose the underlying model this.createdEditorModel = true; } + + isReadonly(): boolean { + return true; + } } \ No newline at end of file diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index dc199593c22..dd26af7c5f2 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -31,7 +31,7 @@ export class TextDiffEditorModel extends DiffEditorModel { return this._modifiedModel as BaseTextEditorModel; } - public load(): TPromise { + load(): TPromise { return super.load().then(() => { this.updateTextDiffEditorModel(); @@ -58,15 +58,19 @@ export class TextDiffEditorModel extends DiffEditorModel { } } - public get textDiffEditorModel(): IDiffEditorModel { + get textDiffEditorModel(): IDiffEditorModel { return this._textDiffEditorModel; } - public isResolved(): boolean { + isResolved(): boolean { return !!this._textDiffEditorModel; } - public dispose(): void { + isReadonly(): boolean { + return this.modifiedModel.isReadonly(); + } + + dispose(): void { // Free the diff editor model but do not propagate the dispose() call to the two models // inside. We never created the two models (original and modified) so we can not dispose @@ -76,4 +80,4 @@ export class TextDiffEditorModel extends DiffEditorModel { super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 3d87656feb7..5f694f64d85 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel, ITextBufferFactory } from 'vs/editor/common/model'; import { IMode } from 'vs/editor/common/modes'; import { EditorModel } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -19,8 +19,10 @@ import { ITextSnapshot } from 'vs/platform/files/common/files'; * The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated. */ export abstract class BaseTextEditorModel extends EditorModel implements ITextEditorModel { - private textEditorModelHandle: URI; + protected createdEditorModel: boolean; + + private textEditorModelHandle: URI; private modelDisposeListener: IDisposable; constructor( @@ -60,10 +62,12 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd }); } - public get textEditorModel(): ITextModel { + get textEditorModel(): ITextModel { return this.textEditorModelHandle ? this.modelService.getModel(this.textEditorModelHandle) : null; } + abstract isReadonly(): boolean; + /** * Creates the text editor model with the provided value, modeId (can be comma separated for multiple values) and optional resource URL. */ @@ -124,7 +128,7 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd this.modelService.updateModel(this.textEditorModel, newValue); } - public createSnapshot(): ITextSnapshot { + createSnapshot(): ITextSnapshot { const model = this.textEditorModel; if (model) { return model.createSnapshot(true /* Preserve BOM */); @@ -133,11 +137,11 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd return null; } - public isResolved(): boolean { + isResolved(): boolean { return !!this.textEditorModelHandle; } - public dispose(): void { + dispose(): void { if (this.modelDisposeListener) { this.modelDisposeListener.dispose(); // dispose this first because it will trigger another dispose() otherwise this.modelDisposeListener = null; diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 6778205ccda..c0728b9cd11 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -5,39 +5,37 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { suggestFilename } from 'vs/base/common/mime'; import { memoize } from 'vs/base/common/decorators'; -import * as labels from 'vs/base/common/labels'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity } from 'vs/workbench/common/editor'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { ILabelService } from 'vs/platform/label/common/label'; /** * An editor input to be used for untitled text buffers. */ export class UntitledEditorInput extends EditorInput implements IEncodingSupport { - public static readonly ID: string = 'workbench.editors.untitledEditorInput'; + static readonly ID: string = 'workbench.editors.untitledEditorInput'; private _hasAssociatedFilePath: boolean; private cachedModel: UntitledEditorModel; private modelResolve: TPromise; - private readonly _onDidModelChangeContent: Emitter; - private readonly _onDidModelChangeEncoding: Emitter; + private readonly _onDidModelChangeContent: Emitter = this._register(new Emitter()); + get onDidModelChangeContent(): Event { return this._onDidModelChangeContent.event; } - private toUnbind: IDisposable[]; + private readonly _onDidModelChangeEncoding: Emitter = this._register(new Emitter()); + get onDidModelChangeEncoding(): Event { return this._onDidModelChangeEncoding.event; } constructor( private resource: URI, @@ -46,41 +44,28 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport private initialValue: string, private preferredEncoding: string, @IInstantiationService private instantiationService: IInstantiationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, @ITextFileService private textFileService: ITextFileService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IHashService private hashService: IHashService + @IHashService private hashService: IHashService, + @ILabelService private labelService: ILabelService ) { super(); this._hasAssociatedFilePath = hasAssociatedFilePath; - this.toUnbind = []; - - this._onDidModelChangeContent = new Emitter(); - this._onDidModelChangeEncoding = new Emitter(); } - public get hasAssociatedFilePath(): boolean { + get hasAssociatedFilePath(): boolean { return this._hasAssociatedFilePath; } - public get onDidModelChangeContent(): Event { - return this._onDidModelChangeContent.event; - } - - public get onDidModelChangeEncoding(): Event { - return this._onDidModelChangeEncoding.event; - } - - public getTypeId(): string { + getTypeId(): string { return UntitledEditorInput.ID; } - public getResource(): URI { + getResource(): URI { return this.resource; } - public getModeId(): string { + getModeId(): string { if (this.cachedModel) { return this.cachedModel.getModeId(); } @@ -88,26 +73,26 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return this.modeId; } - public getName(): string { + getName(): string { return this.hasAssociatedFilePath ? resources.basenameOrAuthority(this.resource) : this.resource.path; } @memoize private get shortDescription(): string { - return paths.basename(labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService)); + return paths.basename(this.labelService.getUriLabel(resources.dirname(this.resource))); } @memoize private get mediumDescription(): string { - return labels.getPathLabel(resources.dirname(this.resource), this.contextService, this.environmentService); + return this.labelService.getUriLabel(resources.dirname(this.resource), true); } @memoize private get longDescription(): string { - return labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService); + return this.labelService.getUriLabel(resources.dirname(this.resource)); } - public getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { + getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { if (!this.hasAssociatedFilePath) { return null; } @@ -136,15 +121,15 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport @memoize private get mediumTitle(): string { - return labels.getPathLabel(this.resource, this.contextService, this.environmentService); + return this.labelService.getUriLabel(this.resource, true); } @memoize private get longTitle(): string { - return labels.getPathLabel(this.resource, void 0, this.environmentService); + return this.labelService.getUriLabel(this.resource); } - public getTitle(verbosity: Verbosity): string { + getTitle(verbosity: Verbosity): string { if (!this.hasAssociatedFilePath) { return this.getName(); } @@ -165,7 +150,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return title; } - public isDirty(): boolean { + isDirty(): boolean { if (this.cachedModel) { return this.cachedModel.isDirty(); } @@ -179,15 +164,15 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return this.hasAssociatedFilePath; } - public confirmSave(): TPromise { + confirmSave(): TPromise { return this.textFileService.confirmSave([this.resource]); } - public save(): TPromise { + save(): TPromise { return this.textFileService.save(this.resource); } - public revert(): TPromise { + revert(): TPromise { if (this.cachedModel) { this.cachedModel.revert(); } @@ -197,7 +182,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return TPromise.as(true); } - public suggestFileName(): string { + suggestFileName(): string { if (!this.hasAssociatedFilePath) { if (this.cachedModel) { const modeId = this.cachedModel.getModeId(); @@ -210,7 +195,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return this.getName(); } - public getEncoding(): string { + getEncoding(): string { if (this.cachedModel) { return this.cachedModel.getEncoding(); } @@ -218,7 +203,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return this.preferredEncoding; } - public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void { + setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void { this.preferredEncoding = encoding; if (this.cachedModel) { @@ -226,7 +211,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport } } - public resolve(): TPromise { + resolve(): TPromise { // Join a model resolve if we have had one before if (this.modelResolve) { @@ -241,17 +226,17 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport } private createModel(): UntitledEditorModel { - const model = this.instantiationService.createInstance(UntitledEditorModel, this.modeId, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding); + const model = this._register(this.instantiationService.createInstance(UntitledEditorModel, this.modeId, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding)); // re-emit some events from the model - this.toUnbind.push(model.onDidChangeContent(() => this._onDidModelChangeContent.fire())); - this.toUnbind.push(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this.toUnbind.push(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); + this._register(model.onDidChangeContent(() => this._onDidModelChangeContent.fire())); + this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); return model; } - public getTelemetryDescriptor(): object { + getTelemetryDescriptor(): object { const descriptor = super.getTelemetryDescriptor(); descriptor['resource'] = telemetryURIDescriptor(this.getResource(), path => this.hashService.createSHA1(path)); @@ -263,7 +248,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return descriptor; } - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } @@ -278,21 +263,9 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return false; } - public dispose(): void { - this._onDidModelChangeContent.dispose(); - this._onDidModelChangeEncoding.dispose(); - - // Listeners - dispose(this.toUnbind); - - // Model - if (this.cachedModel) { - this.cachedModel.dispose(); - this.cachedModel = null; - } - + dispose(): void { this.modelResolve = void 0; super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index c7033b68d95..a13b567ce0d 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { IEncodingSupport } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -23,19 +22,20 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; export class UntitledEditorModel extends BaseTextEditorModel implements IEncodingSupport { - public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; + static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; - private toDispose: IDisposable[]; + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + get onDidChangeContent(): Event { return this._onDidChangeContent.event; } + + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } + + private readonly _onDidChangeEncoding: Emitter = this._register(new Emitter()); + get onDidChangeEncoding(): Event { return this._onDidChangeEncoding.event; } private dirty: boolean; - private readonly _onDidChangeContent: Emitter; - private readonly _onDidChangeDirty: Emitter; - private readonly _onDidChangeEncoding: Emitter; - private versionId: number; - private contentChangeEventScheduler: RunOnceScheduler; - private configuredEncoding: string; constructor( @@ -53,35 +53,12 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin this.dirty = false; this.versionId = 0; - this.toDispose = []; - this._onDidChangeContent = new Emitter(); - this.toDispose.push(this._onDidChangeContent); - - this._onDidChangeDirty = new Emitter(); - this.toDispose.push(this._onDidChangeDirty); - - this._onDidChangeEncoding = new Emitter(); - this.toDispose.push(this._onDidChangeEncoding); - - this.contentChangeEventScheduler = new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY); - this.toDispose.push(this.contentChangeEventScheduler); + this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY)); this.registerListeners(); } - public get onDidChangeContent(): Event { - return this._onDidChangeContent.event; - } - - public get onDidChangeDirty(): Event { - return this._onDidChangeDirty.event; - } - - public get onDidChangeEncoding(): Event { - return this._onDidChangeEncoding.event; - } - protected getOrCreateMode(modeService: IModeService, modeId: string, firstLineText?: string): TPromise { if (!modeId || modeId === PLAINTEXT_MODE_ID) { return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText); // lookup mode via resource path if the provided modeId is unspecific @@ -93,7 +70,7 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin private registerListeners(): void { // Config Changes - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange())); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange())); } private onConfigurationChange(): void { @@ -108,11 +85,11 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin } } - public getVersionId(): number { + getVersionId(): number { return this.versionId; } - public getModeId(): string { + getModeId(): string { if (this.textEditorModel) { return this.textEditorModel.getLanguageIdentifier().language; } @@ -120,11 +97,11 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin return null; } - public getEncoding(): string { + getEncoding(): string { return this.preferredEncoding || this.configuredEncoding; } - public setEncoding(encoding: string): void { + setEncoding(encoding: string): void { const oldEncoding = this.getEncoding(); this.preferredEncoding = encoding; @@ -134,7 +111,7 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin } } - public isDirty(): boolean { + isDirty(): boolean { return this.dirty; } @@ -147,18 +124,18 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin this._onDidChangeDirty.fire(); } - public getResource(): URI { + getResource(): URI { return this.resource; } - public revert(): void { + revert(): void { this.setDirty(false); // Handle content change event buffered this.contentChangeEventScheduler.schedule(); } - public load(): TPromise { + load(): TPromise { // Check for backups first return this.backupFileService.loadBackupResource(this.resource).then(backupResource => { @@ -185,10 +162,10 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin this.configuredEncoding = this.configurationService.getValue(this.resource, 'files.encoding'); // Listen to content changes - this.toDispose.push(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); + this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); // Listen to mode changes - this.toDispose.push(this.textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config + this._register(this.textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config return model; }); @@ -228,9 +205,7 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin this.contentChangeEventScheduler.schedule(); } - public dispose(): void { - super.dispose(); - - this.toDispose = dispose(this.toDispose); + isReadonly(): boolean { + return false; } } diff --git a/src/vs/workbench/common/extensionHostProtocol.ts b/src/vs/workbench/common/extensionHostProtocol.ts new file mode 100644 index 00000000000..70715366c45 --- /dev/null +++ b/src/vs/workbench/common/extensionHostProtocol.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export const enum MessageType { + Initialized, + Ready, + Terminate +} + +export function createMessageOfType(type: MessageType): Buffer { + const result = Buffer.allocUnsafe(1); + + switch (type) { + case MessageType.Initialized: result.writeUInt8(1, 0); break; + case MessageType.Ready: result.writeUInt8(2, 0); break; + case MessageType.Terminate: result.writeUInt8(3, 0); break; + } + + return result; +} + +export function isMessageOfType(message: Buffer, type: MessageType): boolean { + if (message.length !== 1) { + return false; + } + + switch (message.readUInt8(0)) { + case 1: return type === MessageType.Initialized; + case 2: return type === MessageType.Ready; + case 3: return type === MessageType.Terminate; + default: return false; + } +} \ No newline at end of file diff --git a/src/vs/workbench/common/memento.ts b/src/vs/workbench/common/memento.ts index 251a4e7a637..e9ac376a4c9 100644 --- a/src/vs/workbench/common/memento.ts +++ b/src/vs/workbench/common/memento.ts @@ -10,7 +10,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag /** * Supported memento scopes. */ -export enum Scope { +export const enum Scope { /** * The memento will be scoped to all workspaces of this domain. diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 37a912ff71d..7b92f4d048e 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -8,7 +8,7 @@ import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Event, Emitter, once } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isPromiseCanceledError, isErrorWithActions } from 'vs/base/common/errors'; export interface INotificationsModel { @@ -19,7 +19,7 @@ export interface INotificationsModel { notify(notification: INotification): INotificationHandle; } -export enum NotificationChangeType { +export const enum NotificationChangeType { ADD, CHANGE, REMOVE @@ -44,7 +44,9 @@ export interface INotificationChangeEvent { } export class NotificationHandle implements INotificationHandle { + private readonly _onDidClose: Emitter = new Emitter(); + get onDidClose(): Event { return this._onDidClose.event; } constructor(private item: INotificationViewItem, private closeItem: (item: INotificationViewItem) => void) { this.registerListeners(); @@ -57,58 +59,42 @@ export class NotificationHandle implements INotificationHandle { }); } - public get onDidClose(): Event { - return this._onDidClose.event; - } - - public get progress(): INotificationProgress { + get progress(): INotificationProgress { return this.item.progress; } - public updateSeverity(severity: Severity): void { + updateSeverity(severity: Severity): void { this.item.updateSeverity(severity); } - public updateMessage(message: NotificationMessage): void { + updateMessage(message: NotificationMessage): void { this.item.updateMessage(message); } - public updateActions(actions?: INotificationActions): void { + updateActions(actions?: INotificationActions): void { this.item.updateActions(actions); } - public close(): void { + close(): void { this.closeItem(this.item); this._onDidClose.dispose(); } } -export class NotificationsModel implements INotificationsModel { +export class NotificationsModel extends Disposable implements INotificationsModel { private static NO_OP_NOTIFICATION = new NoOpNotification(); - private _notifications: INotificationViewItem[]; + private readonly _onDidNotificationChange: Emitter = this._register(new Emitter()); + get onDidNotificationChange(): Event { return this._onDidNotificationChange.event; } - private readonly _onDidNotificationChange: Emitter; - private toDispose: IDisposable[]; + private _notifications: INotificationViewItem[] = []; - constructor() { - this._notifications = []; - this.toDispose = []; - - this._onDidNotificationChange = new Emitter(); - this.toDispose.push(this._onDidNotificationChange); - } - - public get notifications(): INotificationViewItem[] { + get notifications(): INotificationViewItem[] { return this._notifications; } - public get onDidNotificationChange(): Event { - return this._onDidNotificationChange.event; - } - - public notify(notification: INotification): INotificationHandle { + notify(notification: INotification): INotificationHandle { const item = this.createViewItem(notification); if (!item) { return NotificationsModel.NO_OP_NOTIFICATION; // return early if this is a no-op @@ -187,10 +173,6 @@ export class NotificationsModel implements INotificationsModel { return item; } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } } export interface INotificationViewItem { @@ -226,7 +208,7 @@ export function isNotificationViewItem(obj: any): obj is INotificationViewItem { return obj instanceof NotificationViewItem; } -export enum NotificationViewItemLabelKind { +export const enum NotificationViewItemLabelKind { SEVERITY, MESSAGE, ACTIONS, @@ -250,29 +232,23 @@ export interface INotificationViewItemProgress extends INotificationProgress { dispose(): void; } -export class NotificationViewItemProgress implements INotificationViewItemProgress { +export class NotificationViewItemProgress extends Disposable implements INotificationViewItemProgress { private _state: INotificationViewItemProgressState; - private readonly _onDidChange: Emitter; - private toDispose: IDisposable[]; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + get onDidChange(): Event { return this._onDidChange.event; } constructor() { - this.toDispose = []; - this._state = Object.create(null); + super(); - this._onDidChange = new Emitter(); - this.toDispose.push(this._onDidChange); + this._state = Object.create(null); } - public get state(): INotificationViewItemProgressState { + get state(): INotificationViewItemProgressState { return this._state; } - public get onDidChange(): Event { - return this._onDidChange.event; - } - - public infinite(): void { + infinite(): void { if (this._state.infinite) { return; } @@ -286,7 +262,7 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre this._onDidChange.fire(); } - public done(): void { + done(): void { if (this._state.done) { return; } @@ -300,7 +276,7 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre this._onDidChange.fire(); } - public total(value: number): void { + total(value: number): void { if (this._state.total === value) { return; } @@ -313,7 +289,7 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre this._onDidChange.fire(); } - public worked(value: number): void { + worked(value: number): void { if (typeof this._state.worked === 'number') { this._state.worked += value; } else { @@ -325,10 +301,6 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre this._onDidChange.fire(); } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } } export interface IMessageLink { @@ -345,7 +317,7 @@ export interface INotificationMessage { links: IMessageLink[]; } -export class NotificationViewItem implements INotificationViewItem { +export class NotificationViewItem extends Disposable implements INotificationViewItem { private static MAX_MESSAGE_LENGTH = 1000; @@ -354,16 +326,20 @@ export class NotificationViewItem implements INotificationViewItem { private static LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^\)\s]+)\)/gi; private _expanded: boolean; - private toDispose: IDisposable[]; private _actions: INotificationActions; private _progress: NotificationViewItemProgress; - private readonly _onDidExpansionChange: Emitter; - private readonly _onDidClose: Emitter; - private readonly _onDidLabelChange: Emitter; + private readonly _onDidExpansionChange: Emitter = this._register(new Emitter()); + get onDidExpansionChange(): Event { return this._onDidExpansionChange.event; } - public static create(notification: INotification): INotificationViewItem { + private readonly _onDidClose: Emitter = this._register(new Emitter()); + get onDidClose(): Event { return this._onDidClose.event; } + + private readonly _onDidLabelChange: Emitter = this._register(new Emitter()); + get onDidLabelChange(): Event { return this._onDidLabelChange.event; } + + static create(notification: INotification): INotificationViewItem { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { return null; // we need a message to show } @@ -426,18 +402,9 @@ export class NotificationViewItem implements INotificationViewItem { } private constructor(private _severity: Severity, private _message: INotificationMessage, private _source: string, actions?: INotificationActions) { - this.toDispose = []; + super(); this.setActions(actions); - - this._onDidExpansionChange = new Emitter(); - this.toDispose.push(this._onDidExpansionChange); - - this._onDidLabelChange = new Emitter(); - this.toDispose.push(this._onDidLabelChange); - - this._onDidClose = new Emitter(); - this.toDispose.push(this._onDidClose); } private setActions(actions: INotificationActions): void { @@ -457,62 +424,49 @@ export class NotificationViewItem implements INotificationViewItem { this._expanded = actions.primary.length > 0; } - public get onDidExpansionChange(): Event { - return this._onDidExpansionChange.event; - } - - public get onDidLabelChange(): Event { - return this._onDidLabelChange.event; - } - - public get onDidClose(): Event { - return this._onDidClose.event; - } - - public get canCollapse(): boolean { + get canCollapse(): boolean { return this._actions.primary.length === 0; } - public get expanded(): boolean { + get expanded(): boolean { return this._expanded; } - public get severity(): Severity { + get severity(): Severity { return this._severity; } - public hasProgress(): boolean { + hasProgress(): boolean { return !!this._progress; } - public get progress(): INotificationViewItemProgress { + get progress(): INotificationViewItemProgress { if (!this._progress) { - this._progress = new NotificationViewItemProgress(); - this.toDispose.push(this._progress); - this.toDispose.push(this._progress.onDidChange(() => this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); + this._progress = this._register(new NotificationViewItemProgress()); + this._register(this._progress.onDidChange(() => this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); } return this._progress; } - public get message(): INotificationMessage { + get message(): INotificationMessage { return this._message; } - public get source(): string { + get source(): string { return this._source; } - public get actions(): INotificationActions { + get actions(): INotificationActions { return this._actions; } - public updateSeverity(severity: Severity): void { + updateSeverity(severity: Severity): void { this._severity = severity; this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); } - public updateMessage(input: NotificationMessage): void { + updateMessage(input: NotificationMessage): void { const message = NotificationViewItem.parseNotificationMessage(input); if (!message) { return; @@ -522,13 +476,13 @@ export class NotificationViewItem implements INotificationViewItem { this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); } - public updateActions(actions?: INotificationActions): void { + updateActions(actions?: INotificationActions): void { this.setActions(actions); this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); } - public expand(): void { + expand(): void { if (this._expanded || !this.canCollapse) { return; } @@ -537,7 +491,7 @@ export class NotificationViewItem implements INotificationViewItem { this._onDidExpansionChange.fire(); } - public collapse(skipEvents?: boolean): void { + collapse(skipEvents?: boolean): void { if (!this._expanded || !this.canCollapse) { return; } @@ -549,7 +503,7 @@ export class NotificationViewItem implements INotificationViewItem { } } - public toggle(): void { + toggle(): void { if (this._expanded) { this.collapse(); } else { @@ -557,13 +511,13 @@ export class NotificationViewItem implements INotificationViewItem { } } - public close(): void { + close(): void { this._onDidClose.fire(); - this.toDispose = dispose(this.toDispose); + this.dispose(); } - public equals(other: INotificationViewItem): boolean { + equals(other: INotificationViewItem): boolean { if (this.hasProgress() || other.hasProgress()) { return false; } diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index 4809497f2a8..c109f2e0e8b 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -5,13 +5,15 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IFileService } from 'vs/platform/files/common/files'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; -export class ResourceContextKey implements IContextKey { +export class ResourceContextKey extends Disposable implements IContextKey { static Scheme = new RawContextKey('resourceScheme', undefined); static Filename = new RawContextKey('resourceFilename', undefined); @@ -19,7 +21,8 @@ export class ResourceContextKey implements IContextKey { static Resource = new RawContextKey('resource', undefined); static Extension = new RawContextKey('resourceExtname', undefined); static HasResource = new RawContextKey('resourceSet', false); - static IsFile = new RawContextKey('resourceIsFile', false); + static IsFileSystemResource = new RawContextKey('isFileSystemResource', false); + static IsFileSystemResourceOrUntitled = new RawContextKey('isFileSystemResourceOrUntitled', false); private _resourceKey: IContextKey; private _schemeKey: IContextKey; @@ -27,20 +30,30 @@ export class ResourceContextKey implements IContextKey { private _langIdKey: IContextKey; private _extensionKey: IContextKey; private _hasResource: IContextKey; - private _isFile: IContextKey; + private _isfileSystemResource: IContextKey; + private _isFileSystemResourceOrUntitled: IContextKey; constructor( @IContextKeyService contextKeyService: IContextKeyService, - @IModeService private readonly _modeService: IModeService, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @IModeService private readonly _modeService: IModeService ) { + super(); + this._schemeKey = ResourceContextKey.Scheme.bindTo(contextKeyService); this._filenameKey = ResourceContextKey.Filename.bindTo(contextKeyService); this._langIdKey = ResourceContextKey.LangId.bindTo(contextKeyService); this._resourceKey = ResourceContextKey.Resource.bindTo(contextKeyService); this._extensionKey = ResourceContextKey.Extension.bindTo(contextKeyService); this._hasResource = ResourceContextKey.HasResource.bindTo(contextKeyService); - this._isFile = ResourceContextKey.IsFile.bindTo(contextKeyService); + this._isfileSystemResource = ResourceContextKey.IsFileSystemResource.bindTo(contextKeyService); + this._isFileSystemResourceOrUntitled = ResourceContextKey.IsFileSystemResourceOrUntitled.bindTo(contextKeyService); + + this._register(_fileService.onDidChangeFileSystemProviderRegistrations(() => { + const resource = this._resourceKey.get(); + this._isfileSystemResource.set(resource && _fileService.canHandleResource(resource)); + this._isFileSystemResourceOrUntitled.set(this._isfileSystemResource.get() || this._schemeKey.get() === Schemas.untitled); + })); } set(value: URI) { @@ -50,7 +63,8 @@ export class ResourceContextKey implements IContextKey { this._langIdKey.set(value && this._modeService.getModeIdByFilenameOrFirstLine(value.fsPath)); this._extensionKey.set(value && paths.extname(value.fsPath)); this._hasResource.set(!!value); - this._isFile.set(value && this._fileService.canHandleResource(value)); + this._isfileSystemResource.set(value && this._fileService.canHandleResource(value)); + this._isFileSystemResourceOrUntitled.set(this._isfileSystemResource.get() || this._schemeKey.get() === Schemas.untitled); } reset(): void { @@ -60,10 +74,9 @@ export class ResourceContextKey implements IContextKey { this._langIdKey.reset(); this._extensionKey.reset(); this._hasResource.reset(); - this._isFile.reset(); } - public get(): URI { + get(): URI { return this._resourceKey.get(); } } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fffeb82d9f1..8dbf7693c79 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, listActiveSelectionForeground, listActiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; @@ -64,7 +64,7 @@ export const TAB_ACTIVE_BORDER_TOP = registerColor('tab.activeBorderTop', { dark: null, light: null, hc: null -}, nls.localize('tabActiveBorderTop', "Border to the top of an active tab. The border will not show if a bottom border is already defined (via tab.activeBorder or tab.unfocusedActiveBorder). Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, nls.localize('tabActiveBorderTop', "Border to the top of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER = registerColor('tab.unfocusedActiveBorder', { dark: transparent(TAB_ACTIVE_BORDER, 0.5), @@ -76,7 +76,7 @@ export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiv dark: transparent(TAB_ACTIVE_BORDER_TOP, 0.5), light: transparent(TAB_ACTIVE_BORDER_TOP, 0.7), hc: null -}, nls.localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. The border will not show if a bottom border is already defined (via tab.activeBorder or tab.unfocusedActiveBorder). Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, nls.localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { dark: null, @@ -117,11 +117,17 @@ export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedIna // < --- Editors --- > +export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', { + dark: editorBackground, + light: editorBackground, + hc: editorBackground +}, nls.localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); + registerColor('editorGroup.background', { dark: null, light: null, hc: null -}, nls.localize('editorGroupBackground', "Deprecated background color of an editor group."), false, nls.localize('deprecatedEditorGroupBackground', "Deprecated: Background color of an editor group is no longer being supported with the introduction of the grid editor lyout. You can use editorGroup.emptyBackground to set the background color of empty editor groups.")); +}, nls.localize('editorGroupBackground', "Deprecated background color of an editor group."), false, nls.localize('deprecatedEditorGroupBackground', "Deprecated: Background color of an editor group is no longer being supported with the introduction of the grid editor layout. You can use editorGroup.emptyBackground to set the background color of empty editor groups.")); export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', { dark: null, @@ -161,7 +167,7 @@ export const EDITOR_GROUP_BORDER = registerColor('editorGroup.border', { export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBackground', { dark: Color.fromHex('#53595D').transparent(0.5), - light: Color.fromHex('#3399FF').transparent(0.18), + light: Color.fromHex('#2677CB').transparent(0.18), hc: null }, nls.localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); @@ -188,7 +194,7 @@ export const PANEL_ACTIVE_TITLE_FOREGROUND = registerColor('panelTitle.activeFor }, nls.localize('panelActiveTitleForeground', "Title color for the active panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_INACTIVE_TITLE_FOREGROUND = registerColor('panelTitle.inactiveForeground', { - dark: transparent(PANEL_ACTIVE_TITLE_FOREGROUND, 0.5), + dark: transparent(PANEL_ACTIVE_TITLE_FOREGROUND, 0.6), light: transparent(PANEL_ACTIVE_TITLE_FOREGROUND, 0.75), hc: Color.white }, nls.localize('panelInactiveTitleForeground', "Title color for the inactive panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); @@ -201,7 +207,7 @@ export const PANEL_ACTIVE_TITLE_BORDER = registerColor('panelTitle.activeBorder' export const PANEL_DRAG_AND_DROP_BACKGROUND = registerColor('panel.dropBackground', { dark: Color.white.transparent(0.12), - light: Color.fromHex('#3399FF').transparent(0.18), + light: Color.fromHex('#2677CB').transparent(0.18), hc: Color.white.transparent(0.12) }, nls.localize('panelDragAndDropBackground', "Drag and drop feedback color for the panel title items. The color should have transparency so that the panel entries can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); @@ -388,6 +394,56 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { hc: contrastBorder }, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); +// < --- Menubar --- > + +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { + dark: TITLE_BAR_ACTIVE_FOREGROUND, + light: TITLE_BAR_ACTIVE_FOREGROUND, + hc: TITLE_BAR_ACTIVE_FOREGROUND +}, nls.localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { + dark: transparent(Color.white, 0.1), + light: transparent(Color.black, 0.1), + hc: null +}, nls.localize('menubarSelectionBackground', "Background color of the selected menu item in the menubar.")); + +export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', { + dark: null, + light: null, + hc: activeContrastBorder +}, nls.localize('menubarSelectionBorder', "Border color of the selected menu item in the menubar.")); + +export const MENU_FOREGROUND = registerColor('menu.foreground', { + dark: SIDE_BAR_FOREGROUND, + light: SIDE_BAR_FOREGROUND, + hc: SIDE_BAR_FOREGROUND +}, nls.localize('menuForeground', "Foreground color of menu items.")); + +export const MENU_BACKGROUND = registerColor('menu.background', { + dark: SIDE_BAR_BACKGROUND, + light: SIDE_BAR_BACKGROUND, + hc: SIDE_BAR_BACKGROUND +}, nls.localize('menuBackground', "Background color of menu items.")); + +export const MENU_SELECTION_FOREGROUND = registerColor('menu.selectionForeground', { + dark: listActiveSelectionForeground, + light: listActiveSelectionForeground, + hc: listActiveSelectionForeground +}, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); + +export const MENU_SELECTION_BACKGROUND = registerColor('menu.selectionBackground', { + dark: listActiveSelectionBackground, + light: listActiveSelectionBackground, + hc: listActiveSelectionBackground +}, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); + +export const MENU_SELECTION_BORDER = registerColor('menu.selectionBorder', { + dark: null, + light: null, + hc: null +}, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); + // < --- Notifications --- > export const NOTIFICATIONS_CENTER_BORDER = registerColor('notificationCenter.border', { @@ -455,10 +511,6 @@ export class Themable extends Disposable { this._register(this.themeService.onThemeChange(theme => this.onThemeChange(theme))); } - protected get toUnbind(): IDisposable[] { - return this._toDispose; - } - protected onThemeChange(theme: ITheme): void { this.theme = theme; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index a8559736bfd..408aa711d46 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -42,7 +42,7 @@ export interface IViewContainersRegistry { * * @returns the registered ViewContainer. */ - registerViewContainer(id: string): ViewContainer; + registerViewContainer(id: string, extensionId?: string): ViewContainer; /** * Returns the view container with given id. @@ -54,7 +54,7 @@ export interface IViewContainersRegistry { } export class ViewContainer { - protected constructor(readonly id: string) { } + protected constructor(readonly id: string, readonly extensionId: string) { } } class ViewContainersRegistryImpl implements IViewContainersRegistry { @@ -68,11 +68,11 @@ class ViewContainersRegistryImpl implements IViewContainersRegistry { return values(this.viewContainers); } - registerViewContainer(id: string): ViewContainer { + registerViewContainer(id: string, extensionId: string): ViewContainer { if (!this.viewContainers.has(id)) { const viewContainer = new class extends ViewContainer { constructor() { - super(id); + super(id, extensionId); } }; this.viewContainers.set(id, viewContainer); @@ -96,7 +96,7 @@ export interface IViewDescriptor { readonly container: ViewContainer; - // TODO do we really need this?! + // TODO@Sandeep do we really need this?! readonly ctor: any; readonly when?: ContextKeyExpr; @@ -109,6 +109,7 @@ export interface IViewDescriptor { readonly canToggleVisibility?: boolean; + // Applies only to newly created views readonly hideByDefault?: boolean; } @@ -194,9 +195,15 @@ export const ViewsRegistry: IViewsRegistry = new class implements IViewsRegistry } }; +export interface IView { + + readonly id: string; + +} + export interface IViewsViewlet extends IViewlet { - openView(id: string, focus?: boolean): TPromise; + openView(id: string, focus?: boolean): TPromise; } @@ -205,7 +212,7 @@ export const IViewsService = createDecorator('viewsService'); export interface IViewsService { _serviceBrand: any; - openView(id: string, focus?: boolean): TPromise; + openView(id: string, focus?: boolean): TPromise; } // Custom views @@ -220,6 +227,10 @@ export interface ITreeViewer extends IDisposable { readonly onDidChangeSelection: Event; + readonly onDidChangeVisibility: Event; + + readonly visible: boolean; + refresh(treeItems?: ITreeItem[]): TPromise; setVisibility(visible: boolean): void; @@ -237,7 +248,7 @@ export interface ITreeViewer extends IDisposable { export interface ICustomViewDescriptor extends IViewDescriptor { - treeViewer: ITreeViewer; + readonly treeViewer: ITreeViewer; } @@ -262,9 +273,9 @@ export interface ITreeItem { label?: string; - icon?: string; + icon?: UriComponents; - iconDark?: string; + iconDark?: UriComponents; themeIcon?: ThemeIcon; @@ -277,7 +288,6 @@ export interface ITreeItem { command?: Command; children?: ITreeItem[]; - } export interface ITreeViewDataProvider { diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index 1a54d767562..660b45d0f40 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -7,40 +7,30 @@ import 'vs/css!./media/actions'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; 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 pkg from 'vs/platform/node/package'; import * as errors from 'vs/base/common/errors'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import * as paths from 'vs/base/common/paths'; -import { isMacintosh, isLinux } from 'vs/base/common/platform'; -import { IQuickOpenService, IFilePickOpenEntry, ISeparator, IPickOpenAction, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen'; +import { isMacintosh, isLinux, language } from 'vs/base/common/platform'; import * as browser from 'vs/base/browser/browser'; -import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; -import { IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; -import { ITimerService, IStartupMetrics } from 'vs/workbench/services/timer/common/timerService'; -import { IEditorGroupsService, GroupDirection } from 'vs/workbench/services/group/common/editorGroupsService'; +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 * as os from 'os'; -import { webFrame } from 'electron'; -import { getPathLabel, getBaseLabel } from 'vs/base/common/labels'; +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, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { FileKind } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionService, ActivationTimes } from 'vs/workbench/services/extensions/common/extensions'; -import { getEntries } from 'vs/base/common/performance'; import { IssueType } from 'vs/platform/issue/common/issue'; import { domEvent } from 'vs/base/browser/event'; import { once } from 'vs/base/common/event'; @@ -50,19 +40,25 @@ 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/workbench/browser/labels'; // --- actions export class CloseCurrentWindowAction extends Action { - public static readonly ID = 'workbench.action.closeWindow'; - public static readonly LABEL = nls.localize('closeWindow', "Close Window"); + static readonly ID = 'workbench.action.closeWindow'; + static readonly LABEL = nls.localize('closeWindow', "Close Window"); constructor(id: string, label: string, @IWindowService private windowService: IWindowService) { super(id, label); } - public run(): TPromise { + run(): TPromise { this.windowService.closeWindow(); return TPromise.as(true); @@ -142,7 +138,7 @@ export class ToggleMenuBarAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { let currentVisibilityValue = this.configurationService.getValue(ToggleMenuBarAction.menuBarVisibilityKey); if (typeof currentVisibilityValue !== 'string') { currentVisibilityValue = 'default'; @@ -170,7 +166,7 @@ export class ToggleDevToolsAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.windowsService.toggleDevTools(); } } @@ -198,14 +194,14 @@ export abstract class BaseZoomAction extends Action { browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); }; - this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level).done(() => applyZoom()); + this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level).then(() => applyZoom()); } } export class ZoomInAction extends BaseZoomAction { - public static readonly ID = 'workbench.action.zoomIn'; - public static readonly LABEL = nls.localize('zoomIn', "Zoom In"); + static readonly ID = 'workbench.action.zoomIn'; + static readonly LABEL = nls.localize('zoomIn', "Zoom In"); constructor( id: string, @@ -215,7 +211,7 @@ export class ZoomInAction extends BaseZoomAction { super(id, label, configurationService); } - public run(): TPromise { + run(): TPromise { this.setConfiguredZoomLevel(webFrame.getZoomLevel() + 1); return TPromise.as(true); @@ -224,8 +220,8 @@ export class ZoomInAction extends BaseZoomAction { export class ZoomOutAction extends BaseZoomAction { - public static readonly ID = 'workbench.action.zoomOut'; - public static readonly LABEL = nls.localize('zoomOut', "Zoom Out"); + static readonly ID = 'workbench.action.zoomOut'; + static readonly LABEL = nls.localize('zoomOut', "Zoom Out"); constructor( id: string, @@ -235,7 +231,7 @@ export class ZoomOutAction extends BaseZoomAction { super(id, label, configurationService); } - public run(): TPromise { + run(): TPromise { this.setConfiguredZoomLevel(webFrame.getZoomLevel() - 1); return TPromise.as(true); @@ -244,8 +240,8 @@ export class ZoomOutAction extends BaseZoomAction { export class ZoomResetAction extends BaseZoomAction { - public static readonly ID = 'workbench.action.zoomReset'; - public static readonly LABEL = nls.localize('zoomReset', "Reset Zoom"); + static readonly ID = 'workbench.action.zoomReset'; + static readonly LABEL = nls.localize('zoomReset', "Reset Zoom"); constructor( id: string, @@ -255,287 +251,13 @@ export class ZoomResetAction extends BaseZoomAction { super(id, label, configurationService); } - public run(): TPromise { + run(): TPromise { this.setConfiguredZoomLevel(0); return TPromise.as(true); } } -/* Copied from loader.ts */ -enum LoaderEventType { - LoaderAvailable = 1, - - BeginLoadingScript = 10, - EndLoadingScriptOK = 11, - EndLoadingScriptError = 12, - - BeginInvokeFactory = 21, - EndInvokeFactory = 22, - - NodeBeginEvaluatingScript = 31, - NodeEndEvaluatingScript = 32, - - NodeBeginNativeRequire = 33, - NodeEndNativeRequire = 34 -} - -interface ILoaderEvent { - type: LoaderEventType; - timestamp: number; - detail: string; -} - -export class ShowStartupPerformance extends Action { - - public static readonly ID = 'workbench.action.appPerf'; - public static readonly LABEL = nls.localize('appPerf', "Startup Performance"); - - constructor( - id: string, - label: string, - @IWindowService private windowService: IWindowService, - @ITimerService private timerService: ITimerService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IExtensionService private extensionService: IExtensionService - ) { - super(id, label); - } - - public run(): TPromise { - - // Show dev tools - this.windowService.openDevTools(); - - // Print to console - setTimeout(() => { - (console).group('Startup Performance Measurement'); - const metrics: IStartupMetrics = this.timerService.startupMetrics; - console.log(`OS: ${metrics.platform} (${metrics.release})`); - console.log(`CPUs: ${metrics.cpus.model} (${metrics.cpus.count} x ${metrics.cpus.speed})`); - console.log(`Memory (System): ${(metrics.totalmem / (1024 * 1024 * 1024)).toFixed(2)}GB (${(metrics.freemem / (1024 * 1024 * 1024)).toFixed(2)}GB free)`); - console.log(`Memory (Process): ${(metrics.meminfo.workingSetSize / 1024).toFixed(2)}MB working set (${(metrics.meminfo.peakWorkingSetSize / 1024).toFixed(2)}MB peak, ${(metrics.meminfo.privateBytes / 1024).toFixed(2)}MB private, ${(metrics.meminfo.sharedBytes / 1024).toFixed(2)}MB shared)`); - console.log(`VM (likelyhood): ${metrics.isVMLikelyhood}%`); - console.log(`Initial Startup: ${metrics.initialStartup}`); - console.log(`Screen Reader Active: ${metrics.hasAccessibilitySupport}`); - console.log(`Empty Workspace: ${metrics.emptyWorkbench}`); - - let nodeModuleLoadTime: number; - if (this.environmentService.performance) { - const nodeModuleTimes = this.analyzeNodeModulesLoadTimes(); - nodeModuleLoadTime = nodeModuleTimes.duration; - } - - (console).table(this.getStartupMetricsTable(nodeModuleLoadTime)); - - if (this.environmentService.performance) { - const data = this.analyzeLoaderStats(); - for (let type in data) { - (console).groupCollapsed(`Loader: ${type}`); - (console).table(data[type]); - (console).groupEnd(); - } - } - - (console).groupEnd(); - - (console).group('Extension Activation Stats'); - let extensionsActivationTimes: { [id: string]: ActivationTimes; } = {}; - let extensionsStatus = this.extensionService.getExtensionsStatus(); - for (let id in extensionsStatus) { - const status = extensionsStatus[id]; - if (status.activationTimes) { - extensionsActivationTimes[id] = status.activationTimes; - } - } - (console).table(extensionsActivationTimes); - (console).groupEnd(); - - (console).group('Raw Startup Timers (CSV)'); - let value = `Name\tStart\n`; - let entries = getEntries('mark').slice(0).sort((a, b) => a.startTime - b.startTime); - for (const entry of entries) { - value += `${entry.name}\t${entry.startTime}\n`; - } - console.log(value); - (console).groupEnd(); - }, 1000); - - return TPromise.as(true); - } - - private getStartupMetricsTable(nodeModuleLoadTime?: number): any[] { - const table: any[] = []; - const metrics: IStartupMetrics = this.timerService.startupMetrics; - - if (metrics.initialStartup) { - table.push({ Topic: '[main] start => app.isReady', 'Took (ms)': metrics.timers.ellapsedAppReady }); - table.push({ Topic: '[main] nls:start => nls:end', 'Took (ms)': metrics.timers.ellapsedNlsGeneration }); - table.push({ Topic: '[main] app.isReady => window.loadUrl()', 'Took (ms)': metrics.timers.ellapsedWindowLoad }); - } - - table.push({ Topic: '[renderer] window.loadUrl() => begin to require(workbench.main.js)', 'Took (ms)': metrics.timers.ellapsedWindowLoadToRequire }); - table.push({ Topic: '[renderer] require(workbench.main.js)', 'Took (ms)': metrics.timers.ellapsedRequire }); - - if (nodeModuleLoadTime) { - table.push({ Topic: '[renderer] -> of which require() node_modules', 'Took (ms)': nodeModuleLoadTime }); - } - - table.push({ Topic: '[renderer] create extension host => extensions onReady()', 'Took (ms)': metrics.timers.ellapsedExtensions }); - table.push({ Topic: '[renderer] restore viewlet', 'Took (ms)': metrics.timers.ellapsedViewletRestore }); - table.push({ Topic: '[renderer] restore editor view state', 'Took (ms)': metrics.timers.ellapsedEditorRestore }); - table.push({ Topic: '[renderer] overall workbench load', 'Took (ms)': metrics.timers.ellapsedWorkbench }); - table.push({ Topic: '------------------------------------------------------' }); - table.push({ Topic: '[main, renderer] start => extensions ready', 'Took (ms)': metrics.timers.ellapsedExtensionsReady }); - table.push({ Topic: '[main, renderer] start => workbench ready', 'Took (ms)': metrics.ellapsed }); - - return table; - } - - private analyzeNodeModulesLoadTimes(): { table: any[], duration: number } { - const stats = (require).getStats(); - const result = []; - - let total = 0; - - for (let i = 0, len = stats.length; i < len; i++) { - if (stats[i].type === LoaderEventType.NodeEndNativeRequire) { - if (stats[i - 1].type === LoaderEventType.NodeBeginNativeRequire && stats[i - 1].detail === stats[i].detail) { - const entry: any = {}; - const dur = (stats[i].timestamp - stats[i - 1].timestamp); - entry['Event'] = 'nodeRequire ' + stats[i].detail; - entry['Took (ms)'] = dur.toFixed(2); - total += dur; - entry['Start (ms)'] = '**' + stats[i - 1].timestamp.toFixed(2); - entry['End (ms)'] = '**' + stats[i - 1].timestamp.toFixed(2); - result.push(entry); - } - } - } - - if (total > 0) { - result.push({ Event: '------------------------------------------------------' }); - - const entry: any = {}; - entry['Event'] = '[renderer] total require() node_modules'; - entry['Took (ms)'] = total.toFixed(2); - entry['Start (ms)'] = '**'; - entry['End (ms)'] = '**'; - result.push(entry); - } - - return { table: result, duration: Math.round(total) }; - } - - private analyzeLoaderStats(): { [type: string]: any[] } { - const stats = (require).getStats().slice(0).sort((a: ILoaderEvent, b: ILoaderEvent) => { - if (a.detail < b.detail) { - return -1; - } else if (a.detail > b.detail) { - return 1; - } else if (a.type < b.type) { - return -1; - } else if (a.type > b.type) { - return 1; - } else { - return 0; - } - }); - - class Tick { - - public readonly duration: number; - public readonly detail: string; - - constructor(public readonly start: ILoaderEvent, public readonly end: ILoaderEvent) { - console.assert(start.detail === end.detail); - - this.duration = this.end.timestamp - this.start.timestamp; - this.detail = start.detail; - } - - toTableObject() { - return { - ['Path']: this.start.detail, - ['Took (ms)']: this.duration.toFixed(2), - // ['Start (ms)']: this.start.timestamp, - // ['End (ms)']: this.end.timestamp - }; - } - - static compareUsingStartTimestamp(a: Tick, b: Tick): number { - if (a.start.timestamp < b.start.timestamp) { - return -1; - } else if (a.start.timestamp > b.start.timestamp) { - return 1; - } else { - return 0; - } - } - } - - const ticks: { [type: number]: Tick[] } = { - [LoaderEventType.BeginLoadingScript]: [], - [LoaderEventType.BeginInvokeFactory]: [], - [LoaderEventType.NodeBeginEvaluatingScript]: [], - [LoaderEventType.NodeBeginNativeRequire]: [], - }; - - for (let i = 1; i < stats.length - 1; i++) { - const stat = stats[i]; - const nextStat = stats[i + 1]; - - if (nextStat.type - stat.type > 2) { - //bad?! - break; - } - - i += 1; - ticks[stat.type].push(new Tick(stat, nextStat)); - } - - ticks[LoaderEventType.BeginInvokeFactory].sort(Tick.compareUsingStartTimestamp); - ticks[LoaderEventType.BeginInvokeFactory].sort(Tick.compareUsingStartTimestamp); - ticks[LoaderEventType.NodeBeginEvaluatingScript].sort(Tick.compareUsingStartTimestamp); - ticks[LoaderEventType.NodeBeginNativeRequire].sort(Tick.compareUsingStartTimestamp); - - const ret = { - 'Load Script': ticks[LoaderEventType.BeginLoadingScript].map(t => t.toTableObject()), - '(Node) Load Script': ticks[LoaderEventType.NodeBeginNativeRequire].map(t => t.toTableObject()), - 'Eval Script': ticks[LoaderEventType.BeginInvokeFactory].map(t => t.toTableObject()), - '(Node) Eval Script': ticks[LoaderEventType.NodeBeginEvaluatingScript].map(t => t.toTableObject()), - }; - - function total(ticks: Tick[]): number { - let sum = 0; - for (const tick of ticks) { - sum += tick.duration; - } - return sum; - } - - // totals - ret['Load Script'].push({ - ['Path']: 'TOTAL TIME', - ['Took (ms)']: total(ticks[LoaderEventType.BeginLoadingScript]).toFixed(2) - }); - ret['Eval Script'].push({ - ['Path']: 'TOTAL TIME', - ['Took (ms)']: total(ticks[LoaderEventType.BeginInvokeFactory]).toFixed(2) - }); - ret['(Node) Load Script'].push({ - ['Path']: 'TOTAL TIME', - ['Took (ms)']: total(ticks[LoaderEventType.NodeBeginNativeRequire]).toFixed(2) - }); - ret['(Node) Eval Script'].push({ - ['Path']: 'TOTAL TIME', - ['Took (ms)']: total(ticks[LoaderEventType.NodeBeginEvaluatingScript]).toFixed(2) - }); - - return ret; - } -} - export class ReloadWindowAction extends Action { static readonly ID = 'workbench.action.reloadWindow'; @@ -573,79 +295,62 @@ export class ReloadWindowWithExtensionsDisabledAction extends Action { } export abstract class BaseSwitchWindow extends Action { - private closeWindowAction: CloseWindowAction; + + 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 quickOpenService: IQuickOpenService, + private quickInputService: IQuickInputService, private keybindingService: IKeybindingService, - private instantiationService: IInstantiationService + private modelService: IModelService, + private modeService: IModeService, ) { super(id, label); - this.closeWindowAction = this.instantiationService.createInstance(CloseWindowAction); } protected abstract isQuickNavigate(): boolean; - public run(): TPromise { + run(): TPromise { 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 => ({ - payload: win.id, - resource: win.filename ? URI.file(win.filename) : win.folderPath ? URI.file(win.folderPath) : win.workspace ? URI.file(win.workspace.configPath) : void 0, - fileKind: win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderPath ? FileKind.FOLDER : FileKind.FILE, - label: win.title, - description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : void 0, - run: () => { - setTimeout(() => { - // Bug: somehow when not running this code in a timeout, it is not possible to use this picker - // with quick navigate keys (not able to trigger quick navigate once running it once). - this.windowsService.showWindow(win.id).done(null, errors.onUnexpectedError); - }); - }, - action: (!this.isQuickNavigate() && currentWindowId !== win.id) ? this.closeWindowAction : void 0 - } as IFilePickOpenEntry)); - - this.quickOpenService.pick(picks, { - contextKey: 'inWindowsPicker', - autoFocus: { autoFocusFirstEntry: true }, - placeHolder, - quickNavigateConfiguration: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : void 0 + const picks = windows.map(win => { + const resource = win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? URI.file(win.workspace.configPath) : void 0; + 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") : void 0, + buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : void 0 + } as (IQuickPickItem & { payload: number }); }); - }); - } - public dispose(): void { - super.dispose(); + const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - this.closeWindowAction.dispose(); - } -} - -class CloseWindowAction extends Action implements IPickOpenAction { - - public static readonly ID = 'workbench.action.closeWindow'; - public static readonly LABEL = nls.localize('close', "Close Window"); - - constructor( - @IWindowsService private windowsService: IWindowsService - ) { - super(CloseWindowAction.ID, CloseWindowAction.LABEL); - - this.class = 'action-remove-from-recently-opened'; - } - - public run(item: IPickOpenItem): TPromise { - return this.windowsService.closeWindow(item.getPayload()).then(() => { - item.remove(); - - return true; + return this.quickInputService.pick(picks, { + contextKey: 'inWindowsPicker', + activeItem: picks[autoFocusIndex], + placeHolder, + quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : void 0, + onDidTriggerItemButton: context => { + this.windowsService.closeWindow(context.item.payload).then(() => { + context.removeItem(); + }); + } + }); + }).then(pick => { + if (pick) { + this.windowsService.showWindow(pick.payload); + } }); } } @@ -660,11 +365,12 @@ export class SwitchWindow extends BaseSwitchWindow { label: string, @IWindowsService windowsService: IWindowsService, @IWindowService windowService: IWindowService, - @IQuickOpenService quickOpenService: IQuickOpenService, + @IQuickInputService quickInputService: IQuickInputService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, ) { - super(id, label, windowsService, windowService, quickOpenService, keybindingService, instantiationService); + super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); } protected isQuickNavigate(): boolean { @@ -682,11 +388,12 @@ export class QuickSwitchWindow extends BaseSwitchWindow { label: string, @IWindowsService windowsService: IWindowsService, @IWindowService windowService: IWindowService, - @IQuickOpenService quickOpenService: IQuickOpenService, + @IQuickInputService quickInputService: IQuickInputService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, ) { - super(id, label, windowsService, windowService, quickOpenService, keybindingService, instantiationService); + super(id, label, windowsService, windowService, quickInputService, keybindingService, modelService, modeService); } protected isQuickNavigate(): boolean { @@ -697,128 +404,122 @@ export class QuickSwitchWindow extends BaseSwitchWindow { export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; export abstract class BaseOpenRecentAction extends Action { - private removeAction: RemoveFromRecentlyOpened; + + 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 quickOpenService: IQuickOpenService, + private windowsService: IWindowsService, + private quickInputService: IQuickInputService, private contextService: IWorkspaceContextService, private environmentService: IEnvironmentService, + private labelService: ILabelService, private keybindingService: IKeybindingService, - instantiationService: IInstantiationService + private modelService: IModelService, + private modeService: IModeService, ) { super(id, label); - - this.removeAction = instantiationService.createInstance(RemoveFromRecentlyOpened); } protected abstract isQuickNavigate(): boolean; - public run(): TPromise { + run(): TPromise { return this.windowService.getRecentlyOpened() .then(({ workspaces, files }) => this.openRecent(workspaces, files)); } - private openRecent(recentWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], recentFiles: string[]): void { + private openRecent(recentWorkspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], recentFiles: URI[]): void { - function toPick(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, separator: ISeparator, fileKind: FileKind, environmentService: IEnvironmentService, removeAction?: RemoveFromRecentlyOpened): IFilePickOpenEntry { - let path: string; + const toPick = (workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, fileKind: FileKind, environmentService: IEnvironmentService, labelService: ILabelService, buttons: IQuickInputButton[]) => { + let resource: URI; let label: string; let description: string; - if (isSingleFolderWorkspaceIdentifier(workspace)) { - path = workspace; - label = getBaseLabel(path); - description = getPathLabel(paths.dirname(path), null, environmentService); + 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 { - path = workspace.configPath; - label = getWorkspaceLabel(workspace, environmentService); - description = getPathLabel(paths.dirname(workspace.configPath), null, environmentService); + resource = workspace; + label = getBaseLabel(workspace); + description = labelService.getUriLabel(dirname(resource)); } return { - resource: URI.file(path), - fileKind, + iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), label, description, - separator, - run: context => { - setTimeout(() => { - // Bug: somehow when not running this code in a timeout, it is not possible to use this picker - // with quick navigate keys (not able to trigger quick navigate once running it once). - runPick(path, fileKind === FileKind.FILE, context); - }); - }, - action: removeAction + buttons, + workspace, + resource, + fileKind, }; - } - - const runPick = (path: string, isFile: boolean, context: IEntryRunContext) => { - const forceNewWindow = context.keymods.ctrlCmd; - this.windowService.openWindow([path], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); }; - const workspacePicks: IFilePickOpenEntry[] = recentWorkspaces.map((workspace, index) => toPick(workspace, index === 0 ? { label: nls.localize('workspaces', "workspaces") } : void 0, isSingleFolderWorkspaceIdentifier(workspace) ? FileKind.FOLDER : FileKind.ROOT_FOLDER, this.environmentService, !this.isQuickNavigate() ? this.removeAction : void 0)); - const filePicks: IFilePickOpenEntry[] = recentFiles.map((p, index) => toPick(p, index === 0 ? { label: nls.localize('files', "files"), border: true } : void 0, FileKind.FILE, this.environmentService, !this.isQuickNavigate() ? this.removeAction : void 0)); + 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.environmentService, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : void 0)); + const filePicks = recentFiles.map(p => toPick(p, FileKind.FILE, this.environmentService, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : void 0)); // focus second entry if the first recent workspace is the current workspace let autoFocusSecondEntry: boolean = recentWorkspaces[0] && this.contextService.isCurrentWorkspace(recentWorkspaces[0]); - this.quickOpenService.pick([...workspacePicks, ...filePicks], { + 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, - autoFocus: { autoFocusFirstEntry: !autoFocusSecondEntry, autoFocusSecondEntry: autoFocusSecondEntry }, + 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, - quickNavigateConfiguration: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : void 0 - }).done(null, errors.onUnexpectedError); - } - - public dispose(): void { - super.dispose(); - - this.removeAction.dispose(); - } -} - -class RemoveFromRecentlyOpened extends Action implements IPickOpenAction { - - public static readonly ID = 'workbench.action.removeFromRecentlyOpened'; - public static readonly LABEL = nls.localize('remove', "Remove from Recently Opened"); - - constructor( - @IWindowsService private windowsService: IWindowsService - ) { - super(RemoveFromRecentlyOpened.ID, RemoveFromRecentlyOpened.LABEL); - - this.class = 'action-remove-from-recently-opened'; - } - - public run(item: IPickOpenItem): TPromise { - return this.windowsService.removeFromRecentlyOpened([item.getResource().fsPath]).then(() => { - item.remove(); - - return true; - }); + onKeyMods: mods => keyMods = mods, + quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : void 0, + onDidTriggerItemButton: context => { + this.windowsService.removeFromRecentlyOpened([context.item.workspace]).then(() => { + context.removeItem(); + }).then(null, errors.onUnexpectedError); + } + }) + .then(pick => { + if (pick) { + return runPick(pick.resource, pick.fileKind === FileKind.FILE, keyMods); + } + return null; + }); } } export class OpenRecentAction extends BaseOpenRecentAction { - public static readonly ID = 'workbench.action.openRecent'; - public static readonly LABEL = nls.localize('openRecent', "Open Recent..."); + static readonly ID = 'workbench.action.openRecent'; + static readonly LABEL = nls.localize('openRecent', "Open Recent..."); constructor( id: string, label: string, @IWindowService windowService: IWindowService, - @IQuickOpenService quickOpenService: IQuickOpenService, + @IWindowsService windowsService: IWindowsService, + @IQuickInputService quickInputService: IQuickInputService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEnvironmentService environmentService: IEnvironmentService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + @ILabelService labelService: ILabelService ) { - super(id, label, windowService, quickOpenService, contextService, environmentService, keybindingService, instantiationService); + super(id, label, windowService, windowsService, quickInputService, contextService, environmentService, labelService, keybindingService, modelService, modeService); } protected isQuickNavigate(): boolean { @@ -828,20 +529,23 @@ export class OpenRecentAction extends BaseOpenRecentAction { export class QuickOpenRecentAction extends BaseOpenRecentAction { - public static readonly ID = 'workbench.action.quickOpenRecent'; - public static readonly LABEL = nls.localize('quickOpenRecent', "Quick Open Recent..."); + static readonly ID = 'workbench.action.quickOpenRecent'; + static readonly LABEL = nls.localize('quickOpenRecent', "Quick Open Recent..."); constructor( id: string, label: string, @IWindowService windowService: IWindowService, - @IQuickOpenService quickOpenService: IQuickOpenService, + @IWindowsService windowsService: IWindowsService, + @IQuickInputService quickInputService: IQuickInputService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEnvironmentService environmentService: IEnvironmentService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, + @ILabelService labelService: ILabelService ) { - super(id, label, windowService, quickOpenService, contextService, environmentService, keybindingService, instantiationService); + super(id, label, windowService, windowsService, quickInputService, contextService, environmentService, labelService, keybindingService, modelService, modeService); } protected isQuickNavigate(): boolean { @@ -850,8 +554,8 @@ export class QuickOpenRecentAction extends BaseOpenRecentAction { } export class OpenIssueReporterAction extends Action { - public static readonly ID = 'workbench.action.openIssueReporter'; - public static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); + 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, @@ -861,15 +565,15 @@ export class OpenIssueReporterAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.issueService.openReporter() .then(() => true); } } export class OpenProcessExplorer extends Action { - public static readonly ID = 'workbench.action.openProcessExplorer'; - public static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); + static readonly ID = 'workbench.action.openProcessExplorer'; + static readonly LABEL = nls.localize('openProcessExplorer', "Open Process Explorer"); constructor( id: string, @@ -879,15 +583,15 @@ export class OpenProcessExplorer extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { return this.issueService.openProcessExplorer() .then(() => true); } } export class ReportPerformanceIssueUsingReporterAction extends Action { - public static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; - public static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); + static readonly ID = 'workbench.action.reportPerformanceIssueUsingReporter'; + static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); constructor( id: string, @@ -897,141 +601,21 @@ export class ReportPerformanceIssueUsingReporterAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { // TODO: Reporter should send timings table as well return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }) .then(() => true); } } -// NOTE: This is still used when running --prof-startup, which already opens a dialog, so the reporter is not used. -export class ReportPerformanceIssueAction extends Action { - - public static readonly ID = 'workbench.action.reportPerformanceIssue'; - public static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); - - constructor( - id: string, - label: string, - @IIntegrityService private integrityService: IIntegrityService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ITimerService private timerService: ITimerService - ) { - super(id, label); - } - - public run(appendix?: string): TPromise { - this.integrityService.isPure().then(res => { - const issueUrl = this.generatePerformanceIssueUrl(product.reportIssueUrl, pkg.name, pkg.version, product.commit, product.date, res.isPure, appendix); - - window.open(issueUrl); - }); - - return TPromise.wrap(true); - } - - private generatePerformanceIssueUrl(baseUrl: string, name: string, version: string, commit: string, date: string, isPure: boolean, appendix?: string): string { - - if (!appendix) { - appendix = `Additional Steps to Reproduce (if any): - -1. -2.`; - } - - let nodeModuleLoadTime: number; - if (this.environmentService.performance) { - nodeModuleLoadTime = this.computeNodeModulesLoadTime(); - } - - const metrics: IStartupMetrics = this.timerService.startupMetrics; - - const osVersion = `${os.type()} ${os.arch()} ${os.release()}`; - const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&'; - const body = encodeURIComponent( - `- VSCode Version: ${name} ${version}${isPure ? '' : ' **[Unsupported]**'} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'}) -- OS Version: ${osVersion} -- CPUs: ${metrics.cpus.model} (${metrics.cpus.count} x ${metrics.cpus.speed}) -- Memory (System): ${(metrics.totalmem / (1024 * 1024 * 1024)).toFixed(2)}GB (${(metrics.freemem / (1024 * 1024 * 1024)).toFixed(2)}GB free) -- Memory (Process): ${(metrics.meminfo.workingSetSize / 1024).toFixed(2)}MB working set (${(metrics.meminfo.peakWorkingSetSize / 1024).toFixed(2)}MB peak, ${(metrics.meminfo.privateBytes / 1024).toFixed(2)}MB private, ${(metrics.meminfo.sharedBytes / 1024).toFixed(2)}MB shared) -- Load (avg): ${metrics.loadavg.map(l => Math.round(l)).join(', ')} -- VM: ${metrics.isVMLikelyhood}% -- Initial Startup: ${metrics.initialStartup ? 'yes' : 'no'} -- Screen Reader: ${metrics.hasAccessibilitySupport ? 'yes' : 'no'} -- Empty Workspace: ${metrics.emptyWorkbench ? 'yes' : 'no'} -- Timings: - -${this.generatePerformanceTable(nodeModuleLoadTime)} - ---- - -${appendix}` - ); - - return `${baseUrl}${queryStringPrefix}body=${body}`; - } - - private computeNodeModulesLoadTime(): number { - const stats = (require).getStats(); - let total = 0; - - for (let i = 0, len = stats.length; i < len; i++) { - if (stats[i].type === LoaderEventType.NodeEndNativeRequire) { - if (stats[i - 1].type === LoaderEventType.NodeBeginNativeRequire && stats[i - 1].detail === stats[i].detail) { - const dur = (stats[i].timestamp - stats[i - 1].timestamp); - total += dur; - } - } - } - - return Math.round(total); - } - - private generatePerformanceTable(nodeModuleLoadTime?: number): string { - let tableHeader = `|Component|Task|Time (ms)| -|---|---|---|`; - - const table = this.getStartupMetricsTable(nodeModuleLoadTime).map(e => { - return `|${e.component}|${e.task}|${e.time}|`; - }).join('\n'); - - return `${tableHeader}\n${table}`; - } - - private getStartupMetricsTable(nodeModuleLoadTime?: number): { component: string, task: string; time: number; }[] { - const table: any[] = []; - const metrics: IStartupMetrics = this.timerService.startupMetrics; - - if (metrics.initialStartup) { - table.push({ component: 'main', task: 'start => app.isReady', time: metrics.timers.ellapsedAppReady }); - table.push({ component: 'main', task: 'app.isReady => window.loadUrl()', time: metrics.timers.ellapsedWindowLoad }); - } - - table.push({ component: 'renderer', task: 'window.loadUrl() => begin to require(workbench.main.js)', time: metrics.timers.ellapsedWindowLoadToRequire }); - table.push({ component: 'renderer', task: 'require(workbench.main.js)', time: metrics.timers.ellapsedRequire }); - - if (nodeModuleLoadTime) { - table.push({ component: 'renderer', task: '-> of which require() node_modules', time: nodeModuleLoadTime }); - } - - table.push({ component: 'renderer', task: 'create extension host => extensions onReady()', time: metrics.timers.ellapsedExtensions }); - table.push({ component: 'renderer', task: 'restore viewlet', time: metrics.timers.ellapsedViewletRestore }); - table.push({ component: 'renderer', task: 'restore editor view state', time: metrics.timers.ellapsedEditorRestore }); - table.push({ component: 'renderer', task: 'overall workbench load', time: metrics.timers.ellapsedWorkbench }); - table.push({ component: 'main + renderer', task: 'start => extensions ready', time: metrics.timers.ellapsedExtensionsReady }); - table.push({ component: 'main + renderer', task: 'start => workbench ready', time: metrics.ellapsed }); - - return table; - } -} export class KeybindingsReferenceAction extends Action { - public static readonly ID = 'workbench.action.keybindingsReference'; - public static readonly LABEL = nls.localize('keybindingsReference', "Keyboard Shortcuts Reference"); + 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; - public static readonly AVAILABLE = !!KeybindingsReferenceAction.URL; + static readonly AVAILABLE = !!KeybindingsReferenceAction.URL; constructor( id: string, @@ -1040,7 +624,7 @@ export class KeybindingsReferenceAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { window.open(KeybindingsReferenceAction.URL); return null; } @@ -1048,11 +632,11 @@ export class KeybindingsReferenceAction extends Action { export class OpenDocumentationUrlAction extends Action { - public static readonly ID = 'workbench.action.openDocumentationUrl'; - public static readonly LABEL = nls.localize('openDocumentationUrl', "Documentation"); + static readonly ID = 'workbench.action.openDocumentationUrl'; + static readonly LABEL = nls.localize('openDocumentationUrl', "Documentation"); private static readonly URL = product.documentationUrl; - public static readonly AVAILABLE = !!OpenDocumentationUrlAction.URL; + static readonly AVAILABLE = !!OpenDocumentationUrlAction.URL; constructor( id: string, @@ -1061,7 +645,7 @@ export class OpenDocumentationUrlAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { window.open(OpenDocumentationUrlAction.URL); return null; } @@ -1069,11 +653,11 @@ export class OpenDocumentationUrlAction extends Action { export class OpenIntroductoryVideosUrlAction extends Action { - public static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; - public static readonly LABEL = nls.localize('openIntroductoryVideosUrl', "Introductory Videos"); + static readonly ID = 'workbench.action.openIntroductoryVideosUrl'; + static readonly LABEL = nls.localize('openIntroductoryVideosUrl', "Introductory Videos"); private static readonly URL = product.introductoryVideosUrl; - public static readonly AVAILABLE = !!OpenIntroductoryVideosUrlAction.URL; + static readonly AVAILABLE = !!OpenIntroductoryVideosUrlAction.URL; constructor( id: string, @@ -1082,7 +666,7 @@ export class OpenIntroductoryVideosUrlAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { window.open(OpenIntroductoryVideosUrlAction.URL); return null; } @@ -1090,11 +674,11 @@ export class OpenIntroductoryVideosUrlAction extends Action { export class OpenTipsAndTricksUrlAction extends Action { - public static readonly ID = 'workbench.action.openTipsAndTricksUrl'; - public static readonly LABEL = nls.localize('openTipsAndTricksUrl', "Tips and Tricks"); + static readonly ID = 'workbench.action.openTipsAndTricksUrl'; + static readonly LABEL = nls.localize('openTipsAndTricksUrl', "Tips and Tricks"); private static readonly URL = product.tipsAndTricksUrl; - public static readonly AVAILABLE = !!OpenTipsAndTricksUrlAction.URL; + static readonly AVAILABLE = !!OpenTipsAndTricksUrlAction.URL; constructor( id: string, @@ -1103,7 +687,7 @@ export class OpenTipsAndTricksUrlAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { window.open(OpenTipsAndTricksUrlAction.URL); return null; } @@ -1123,7 +707,7 @@ export class ToggleSharedProcessAction extends Action { } } -export enum Direction { +export const enum Direction { Next, Previous, } @@ -1141,7 +725,7 @@ export abstract class BaseNavigationAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const isEditorFocus = this.partService.hasFocus(Parts.EDITOR_PART); const isPanelFocus = this.partService.hasFocus(Parts.PANEL_PART); const isSidebarFocus = this.partService.hasFocus(Parts.SIDEBAR_PART); @@ -1197,27 +781,29 @@ export abstract class BaseNavigationAction extends Action { } protected navigateAcrossEditorGroup(direction: GroupDirection): TPromise { - const nextGroup = this.editorGroupService.findGroup({ direction }, this.editorGroupService.activeGroup); - if (nextGroup) { - nextGroup.focus(); + return this.doNavigateToEditorGroup({ direction }); + } + + protected navigateToEditorGroup(location: GroupLocation): TPromise { + return this.doNavigateToEditorGroup({ location }); + } + + private doNavigateToEditorGroup(scope: IFindGroupScope): TPromise { + const targetGroup = this.editorGroupService.findGroup(scope, this.editorGroupService.activeGroup); + if (targetGroup) { + targetGroup.focus(); return TPromise.as(true); } return TPromise.as(false); } - - protected navigateToActiveEditorGroup(): TPromise { - this.editorGroupService.activeGroup.focus(); - - return TPromise.as(true); - } } export class NavigateLeftAction extends BaseNavigationAction { - public static readonly ID = 'workbench.action.navigateLeft'; - public static readonly LABEL = nls.localize('navigateLeft', "Navigate to the View on the Left"); + static readonly ID = 'workbench.action.navigateLeft'; + static readonly LABEL = nls.localize('navigateLeft', "Navigate to the View on the Left"); constructor( id: string, @@ -1251,7 +837,7 @@ export class NavigateLeftAction extends BaseNavigationAction { } if (!isPanelPositionDown) { - return this.navigateToActiveEditorGroup(); + return this.navigateToEditorGroup(GroupLocation.LAST); } return TPromise.as(false); @@ -1259,7 +845,7 @@ export class NavigateLeftAction extends BaseNavigationAction { protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): TPromise { if (!isSidebarPositionLeft) { - return this.navigateToActiveEditorGroup(); + return this.navigateToEditorGroup(GroupLocation.LAST); } return TPromise.as(false); @@ -1268,8 +854,8 @@ export class NavigateLeftAction extends BaseNavigationAction { export class NavigateRightAction extends BaseNavigationAction { - public static readonly ID = 'workbench.action.navigateRight'; - public static readonly LABEL = nls.localize('navigateRight', "Navigate to the View on the Right"); + static readonly ID = 'workbench.action.navigateRight'; + static readonly LABEL = nls.localize('navigateRight', "Navigate to the View on the Right"); constructor( id: string, @@ -1311,7 +897,7 @@ export class NavigateRightAction extends BaseNavigationAction { protected navigateOnSidebarFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): TPromise { if (isSidebarPositionLeft) { - return this.navigateToActiveEditorGroup(); + return this.navigateToEditorGroup(GroupLocation.FIRST); } return TPromise.as(false); @@ -1320,8 +906,8 @@ export class NavigateRightAction extends BaseNavigationAction { export class NavigateUpAction extends BaseNavigationAction { - public static readonly ID = 'workbench.action.navigateUp'; - public static readonly LABEL = nls.localize('navigateUp', "Navigate to the View Above"); + static readonly ID = 'workbench.action.navigateUp'; + static readonly LABEL = nls.localize('navigateUp', "Navigate to the View Above"); constructor( id: string, @@ -1340,7 +926,7 @@ export class NavigateUpAction extends BaseNavigationAction { protected navigateOnPanelFocus(isSidebarPositionLeft: boolean, isPanelPositionDown: boolean): TPromise { if (isPanelPositionDown) { - return this.navigateToActiveEditorGroup(); + return this.navigateToEditorGroup(GroupLocation.LAST); } return TPromise.as(false); @@ -1349,8 +935,8 @@ export class NavigateUpAction extends BaseNavigationAction { export class NavigateDownAction extends BaseNavigationAction { - public static readonly ID = 'workbench.action.navigateDown'; - public static readonly LABEL = nls.localize('navigateDown', "Navigate to the View Below"); + static readonly ID = 'workbench.action.navigateDown'; + static readonly LABEL = nls.localize('navigateDown', "Navigate to the View Below"); constructor( id: string, @@ -1415,8 +1001,8 @@ export abstract class BaseResizeViewAction extends Action { export class IncreaseViewSizeAction extends BaseResizeViewAction { - public static readonly ID = 'workbench.action.increaseViewSize'; - public static readonly LABEL = nls.localize('increaseViewSize', "Increase Current View Size"); + static readonly ID = 'workbench.action.increaseViewSize'; + static readonly LABEL = nls.localize('increaseViewSize', "Increase Current View Size"); constructor( id: string, @@ -1426,7 +1012,7 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { super(id, label, partService); } - public run(): TPromise { + run(): TPromise { this.resizePart(BaseResizeViewAction.RESIZE_INCREMENT); return TPromise.as(true); } @@ -1434,8 +1020,8 @@ export class IncreaseViewSizeAction extends BaseResizeViewAction { export class DecreaseViewSizeAction extends BaseResizeViewAction { - public static readonly ID = 'workbench.action.decreaseViewSize'; - public static readonly LABEL = nls.localize('decreaseViewSize', "Decrease Current View Size"); + static readonly ID = 'workbench.action.decreaseViewSize'; + static readonly LABEL = nls.localize('decreaseViewSize', "Decrease Current View Size"); constructor( id: string, @@ -1446,16 +1032,34 @@ export class DecreaseViewSizeAction extends BaseResizeViewAction { super(id, label, partService); } - public run(): TPromise { + run(): TPromise { this.resizePart(-BaseResizeViewAction.RESIZE_INCREMENT); return TPromise.as(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 windowsService: IWindowsService + ) { + super(NewWindowTab.ID, NewWindowTab.LABEL); + } + + run(): TPromise { + return this.windowsService.newWindowTab().then(() => true); + } +} + export class ShowPreviousWindowTab extends Action { - public static readonly ID = 'workbench.action.showPreviousWindowTab'; - public static readonly LABEL = nls.localize('showPreviousTab', "Show Previous Window Tab"); + static readonly ID = 'workbench.action.showPreviousWindowTab'; + static readonly LABEL = nls.localize('showPreviousTab', "Show Previous Window Tab"); constructor( id: string, @@ -1465,15 +1069,15 @@ export class ShowPreviousWindowTab extends Action { super(ShowPreviousWindowTab.ID, ShowPreviousWindowTab.LABEL); } - public run(): TPromise { + run(): TPromise { return this.windowsService.showPreviousWindowTab().then(() => true); } } export class ShowNextWindowTab extends Action { - public static readonly ID = 'workbench.action.showNextWindowTab'; - public static readonly LABEL = nls.localize('showNextWindowTab', "Show Next Window Tab"); + static readonly ID = 'workbench.action.showNextWindowTab'; + static readonly LABEL = nls.localize('showNextWindowTab', "Show Next Window Tab"); constructor( id: string, @@ -1483,15 +1087,15 @@ export class ShowNextWindowTab extends Action { super(ShowNextWindowTab.ID, ShowNextWindowTab.LABEL); } - public run(): TPromise { + run(): TPromise { return this.windowsService.showNextWindowTab().then(() => true); } } export class MoveWindowTabToNewWindow extends Action { - public static readonly ID = 'workbench.action.moveWindowTabToNewWindow'; - public static readonly LABEL = nls.localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"); + static readonly ID = 'workbench.action.moveWindowTabToNewWindow'; + static readonly LABEL = nls.localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"); constructor( id: string, @@ -1501,15 +1105,15 @@ export class MoveWindowTabToNewWindow extends Action { super(MoveWindowTabToNewWindow.ID, MoveWindowTabToNewWindow.LABEL); } - public run(): TPromise { + run(): TPromise { return this.windowsService.moveWindowTabToNewWindow().then(() => true); } } export class MergeAllWindowTabs extends Action { - public static readonly ID = 'workbench.action.mergeAllWindowTabs'; - public static readonly LABEL = nls.localize('mergeAllWindowTabs', "Merge All Windows"); + static readonly ID = 'workbench.action.mergeAllWindowTabs'; + static readonly LABEL = nls.localize('mergeAllWindowTabs', "Merge All Windows"); constructor( id: string, @@ -1519,15 +1123,15 @@ export class MergeAllWindowTabs extends Action { super(MergeAllWindowTabs.ID, MergeAllWindowTabs.LABEL); } - public run(): TPromise { + run(): TPromise { return this.windowsService.mergeAllWindowTabs().then(() => true); } } export class ToggleWindowTabsBar extends Action { - public static readonly ID = 'workbench.action.toggleWindowTabsBar'; - public static readonly LABEL = nls.localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"); + static readonly ID = 'workbench.action.toggleWindowTabsBar'; + static readonly LABEL = nls.localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"); constructor( id: string, @@ -1537,15 +1141,111 @@ export class ToggleWindowTabsBar extends Action { super(ToggleWindowTabsBar.ID, ToggleWindowTabsBar.LABEL); } - public run(): TPromise { + run(): TPromise { 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(): TPromise { + if (product.twitterUrl) { + return TPromise.as(shell.openExternal(product.twitterUrl)); + } + + return TPromise.as(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(): TPromise { + if (product.requestFeatureUrl) { + return TPromise.as(shell.openExternal(product.requestFeatureUrl)); + } + + return TPromise.as(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(): TPromise { + if (product.licenseUrl) { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + return TPromise.as(shell.openExternal(`${product.licenseUrl}${queryArgChar}lang=${language}`)); + } else { + return TPromise.as(shell.openExternal(product.licenseUrl)); + } + } + + return TPromise.as(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(): TPromise { + if (product.privacyStatementUrl) { + if (language) { + const queryArgChar = product.privacyStatementUrl.indexOf('?') > 0 ? '&' : '?'; + return TPromise.as(shell.openExternal(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`)); + } else { + return TPromise.as(shell.openExternal(product.privacyStatementUrl)); + } + } + + + return TPromise.as(false); + } +} + export class ShowAboutDialogAction extends Action { - public static readonly ID = 'workbench.action.showAboutDialog'; - public static LABEL = nls.localize('about', "About {0}", product.applicationName); + static readonly ID = 'workbench.action.showAboutDialog'; + static LABEL = nls.localize('about', "About {0}", product.applicationName); constructor( id: string, @@ -1562,8 +1262,8 @@ export class ShowAboutDialogAction extends Action { export class InspectContextKeysAction extends Action { - public static readonly ID = 'workbench.action.inspectContextKeys'; - public static LABEL = nls.localize('inspect context keys', "Inspect Context Keys"); + static readonly ID = 'workbench.action.inspectContextKeys'; + static LABEL = nls.localize('inspect context keys', "Inspect Context Keys"); constructor( id: string, diff --git a/src/vs/workbench/electron-browser/bootstrap/index.html b/src/vs/workbench/electron-browser/bootstrap/index.html deleted file mode 100644 index c14ebbe8653..00000000000 --- a/src/vs/workbench/electron-browser/bootstrap/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/bootstrap/index.js b/src/vs/workbench/electron-browser/bootstrap/index.js deleted file mode 100644 index 75f6dc4d7f6..00000000000 --- a/src/vs/workbench/electron-browser/bootstrap/index.js +++ /dev/null @@ -1,265 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Warning: Do not use the `let` declarator in this file, it breaks our minification - -'use strict'; - -/*global window,document,define*/ - -const perf = require('../../../base/common/performance'); -perf.mark('renderer/started'); - -const path = require('path'); -const fs = require('fs'); -const electron = require('electron'); -const remote = electron.remote; -const ipc = electron.ipcRenderer; - -process.lazyEnv = new Promise(function (resolve) { - const handle = setTimeout(function () { - resolve(); - console.warn('renderer did not receive lazyEnv in time'); - }, 10000); - ipc.once('vscode:acceptShellEnv', function (event, shellEnv) { - clearTimeout(handle); - assign(process.env, shellEnv); - resolve(process.env); - }); - ipc.send('vscode:fetchShellEnv'); -}); - -Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) - -function onError(error, enableDeveloperTools) { - if (enableDeveloperTools) { - remote.getCurrentWebContents().openDevTools(); - } - - console.error('[uncaught exception]: ' + error); - - if (error.stack) { - console.error(error.stack); - } -} - -function assign(destination, source) { - return Object.keys(source) - .reduce(function (r, key) { r[key] = source[key]; return r; }, destination); -} - -function parseURLQueryArgs() { - const search = window.location.search || ''; - - return search.split(/[?&]/) - .filter(function (param) { return !!param; }) - .map(function (param) { return param.split('='); }) - .filter(function (param) { return param.length === 2; }) - .reduce(function (r, param) { r[param[0]] = decodeURIComponent(param[1]); return r; }, {}); -} - -function uriFromPath(_path) { - var pathName = path.resolve(_path).replace(/\\/g, '/'); - if (pathName.length > 0 && pathName.charAt(0) !== '/') { - pathName = '/' + pathName; - } - - return encodeURI('file://' + pathName); -} - -function readFile(file) { - return new Promise(function(resolve, reject) { - fs.readFile(file, 'utf8', function(err, data) { - if (err) { - reject(err); - return; - } - resolve(data); - }); - }); -} - -function registerListeners(enableDeveloperTools) { - - // Devtools & reload support - var listener; - if (enableDeveloperTools) { - const extractKey = function (e) { - return [ - e.ctrlKey ? 'ctrl-' : '', - e.metaKey ? 'meta-' : '', - e.altKey ? 'alt-' : '', - e.shiftKey ? 'shift-' : '', - e.keyCode - ].join(''); - }; - - const TOGGLE_DEV_TOOLS_KB = (process.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I - const RELOAD_KB = (process.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R - - listener = function (e) { - const key = extractKey(e); - if (key === TOGGLE_DEV_TOOLS_KB) { - remote.getCurrentWebContents().toggleDevTools(); - } else if (key === RELOAD_KB) { - remote.getCurrentWindow().reload(); - } - }; - window.addEventListener('keydown', listener); - } - - process.on('uncaughtException', function (error) { onError(error, enableDeveloperTools); }); - - return function () { - if (listener) { - window.removeEventListener('keydown', listener); - listener = void 0; - } - }; -} - -function main() { - const webFrame = require('electron').webFrame; - const args = parseURLQueryArgs(); - const configuration = JSON.parse(args['config'] || '{}') || {}; - - //#region Add support for using node_modules.asar - (function () { - const path = require('path'); - const Module = require('module'); - let NODE_MODULES_PATH = path.join(configuration.appRoot, 'node_modules'); - if (/[a-z]\:/.test(NODE_MODULES_PATH)) { - // Make drive letter uppercase - NODE_MODULES_PATH = NODE_MODULES_PATH.charAt(0).toUpperCase() + NODE_MODULES_PATH.substr(1); - } - const NODE_MODULES_ASAR_PATH = NODE_MODULES_PATH + '.asar'; - - const originalResolveLookupPaths = Module._resolveLookupPaths; - Module._resolveLookupPaths = function (request, parent, newReturn) { - const result = originalResolveLookupPaths(request, parent, newReturn); - - const paths = newReturn ? result : result[1]; - for (let i = 0, len = paths.length; i < len; i++) { - if (paths[i] === NODE_MODULES_PATH) { - paths.splice(i, 0, NODE_MODULES_ASAR_PATH); - break; - } - } - - return result; - }; - })(); - //#endregion - - // Correctly inherit the parent's environment - assign(process.env, configuration.userEnv); - perf.importEntries(configuration.perfEntries); - - // Get the nls configuration into the process.env as early as possible. - var nlsConfig = { availableLanguages: {} }; - const config = process.env['VSCODE_NLS_CONFIG']; - if (config) { - process.env['VSCODE_NLS_CONFIG'] = config; - try { - nlsConfig = JSON.parse(config); - } catch (e) { /*noop*/ } - } - - if (nlsConfig._resolvedLanguagePackCoreLocation) { - let bundles = Object.create(null); - nlsConfig.loadBundle = function(bundle, language, cb) { - let result = bundles[bundle]; - if (result) { - cb(undefined, result); - return; - } - let bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, bundle.replace(/\//g, '!') + '.nls.json'); - readFile(bundleFile).then(function (content) { - let json = JSON.parse(content); - bundles[bundle] = json; - cb(undefined, json); - }) - .catch(cb); - }; - } - - var locale = nlsConfig.availableLanguages['*'] || 'en'; - if (locale === 'zh-tw') { - locale = 'zh-Hant'; - } else if (locale === 'zh-cn') { - locale = 'zh-Hans'; - } - window.document.documentElement.setAttribute('lang', locale); - - const enableDeveloperTools = (process.env['VSCODE_DEV'] || !!configuration.extensionDevelopmentPath) && !configuration.extensionTestsPath; - const unbind = registerListeners(enableDeveloperTools); - - // disable pinch zoom & apply zoom level early to avoid glitches - const zoomLevel = configuration.zoomLevel; - webFrame.setVisualZoomLevelLimits(1, 1); - if (typeof zoomLevel === 'number' && zoomLevel !== 0) { - webFrame.setZoomLevel(zoomLevel); - } - - // Load the loader and start loading the workbench - const loaderFilename = configuration.appRoot + '/out/vs/loader.js'; - const loaderSource = require('fs').readFileSync(loaderFilename); - require('vm').runInThisContext(loaderSource, { filename: loaderFilename }); - var define = global.define; - global.define = undefined; - - window.nodeRequire = require.__$__nodeRequire; - - define('fs', ['original-fs'], function (originalFS) { return originalFS; }); // replace the patched electron fs with the original node fs for all AMD code - - window.MonacoEnvironment = {}; - - const onNodeCachedData = window.MonacoEnvironment.onNodeCachedData = []; - require.config({ - baseUrl: uriFromPath(configuration.appRoot) + '/out', - 'vs/nls': nlsConfig, - recordStats: !!configuration.performance, - nodeCachedDataDir: configuration.nodeCachedDataDir, - onNodeCachedData: function () { onNodeCachedData.push(arguments); }, - nodeModules: [/*BUILD->INSERT_NODE_MODULES*/] - }); - - if (nlsConfig.pseudo) { - require(['vs/nls'], function (nlsPlugin) { - nlsPlugin.setPseudoTranslation(nlsConfig.pseudo); - }); - } - - // Perf Counters - window.MonacoEnvironment.timers = { - isInitialStartup: !!configuration.isInitialStartup, - hasAccessibilitySupport: !!configuration.accessibilitySupport, - start: configuration.perfStartTime, - windowLoad: configuration.perfWindowLoadTime - }; - - perf.mark('willLoadWorkbenchMain'); - require([ - 'vs/workbench/workbench.main', - 'vs/nls!vs/workbench/workbench.main', - 'vs/css!vs/workbench/workbench.main' - ], function () { - perf.mark('didLoadWorkbenchMain'); - - process.lazyEnv.then(function () { - perf.mark('main/startup'); - require('vs/workbench/electron-browser/main') - .startup(configuration) - .done(function () { - unbind(); // since the workbench is running, unbind our developer related listeners and let the workbench handle them - }, function (error) { - onError(error, enableDeveloperTools); - }); - }); - }); - -} - -main(); diff --git a/src/vs/workbench/electron-browser/bootstrap/preload.js b/src/vs/workbench/electron-browser/bootstrap/preload.js deleted file mode 100644 index d451c2d5fb7..00000000000 --- a/src/vs/workbench/electron-browser/bootstrap/preload.js +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -(function() { - function getConfig() { - const queryParams = window.location.search.substring(1).split('&'); - for (var i = 0; i < queryParams.length; i++) { - var kv = queryParams[i].split('='); - if (kv[0] === 'config' && kv[1]) { - return JSON.parse(decodeURIComponent(kv[1])); - } - } - return {}; - } - try { - const config = getConfig(); - const document = window.document; - - // sets the base theme class ('vs', 'vs-dark', 'hc-black') - const baseTheme = config.baseTheme || 'vs'; - document.body.className = 'monaco-shell ' + baseTheme; - - // adds a stylesheet with the backgrdound color - var backgroundColor = config.backgroundColor; - if (!backgroundColor) { - backgroundColor = baseTheme === 'hc-black' ? '#000000' : (baseTheme === 'vs' ? '#FFFFFF' : '#1E1E1E'); - } - const foregroundColor = baseTheme === 'hc-black' ? '#FFFFFF' : (baseTheme === 'vs' ? '#6C6C6C' : '#CCCCCC'); - const style = document.createElement('style'); - style.innerHTML = '.monaco-shell { background-color:' + backgroundColor + '; color:' + foregroundColor + '; }'; - document.head.appendChild(style); - - } catch (error) { - console.error(error); - } -})(); \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/commands.ts b/src/vs/workbench/electron-browser/commands.ts index ecb6e315f43..fe2277003fd 100644 --- a/src/vs/workbench/electron-browser/commands.ts +++ b/src/vs/workbench/electron-browser/commands.ts @@ -7,18 +7,19 @@ import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +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 * as errors from 'vs/base/common/errors'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget } from 'vs/platform/list/browser/listService'; +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'; // --- List Commands @@ -32,6 +33,7 @@ function ensureDOMFocus(widget: ListWidget): void { } } +export const QUIT_ID = 'workbench.action.quit'; export function registerCommands(): void { function focusDown(accessor: ServicesAccessor, arg2?: number): void { @@ -57,13 +59,13 @@ export function registerCommands(): void { const tree = focused; tree.focusNext(count, { origin: 'keyboard' }); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusDown', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.DownArrow, mac: { @@ -104,8 +106,8 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expandSelectionDown', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), primary: KeyMod.Shift | KeyCode.DownArrow, handler: (accessor, arg2) => { const focused = accessor.get(IListService).lastFocusedList; @@ -159,13 +161,13 @@ export function registerCommands(): void { const tree = focused; tree.focusPrevious(count, { origin: 'keyboard' }); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusUp', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.UpArrow, mac: { @@ -177,8 +179,8 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expandSelectionUp', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), primary: KeyMod.Shift | KeyCode.UpArrow, handler: (accessor, arg2) => { const focused = accessor.get(IListService).lastFocusedList; @@ -211,7 +213,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.collapse', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.LeftArrow, mac: { @@ -234,14 +236,14 @@ export function registerCommands(): void { } return void 0; - }).done(null, errors.onUnexpectedError); + }); } } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expand', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.RightArrow, handler: (accessor) => { @@ -260,14 +262,14 @@ export function registerCommands(): void { } return void 0; - }).done(null, errors.onUnexpectedError); + }); } } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusPageUp', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.PageUp, handler: (accessor) => { @@ -289,14 +291,14 @@ export function registerCommands(): void { const tree = focused; tree.focusPreviousPage({ origin: 'keyboard' }); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusPageDown', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.PageDown, handler: (accessor) => { @@ -318,14 +320,14 @@ export function registerCommands(): void { const tree = focused; tree.focusNextPage({ origin: 'keyboard' }); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusFirst', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.Home, handler: accessor => listFocusFirst(accessor) @@ -333,7 +335,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusFirstChild', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: null, handler: accessor => listFocusFirst(accessor, { fromFocused: true }) @@ -358,13 +360,13 @@ export function registerCommands(): void { const tree = focused; tree.focusFirst({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : void 0); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusLast', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.End, handler: accessor => listFocusLast(accessor) @@ -372,7 +374,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.focusLastChild', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: null, handler: accessor => listFocusLast(accessor, { fromFocused: true }) @@ -397,13 +399,13 @@ export function registerCommands(): void { const tree = focused; tree.focusLast({ origin: 'keyboard' }, options && options.fromFocused ? tree.getFocus() : void 0); - tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); + tree.reveal(tree.getFocus()); } } KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.select', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.Enter, mac: { @@ -434,7 +436,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.selectAll', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListSupportsMultiSelectContextKey), primary: KeyMod.CtrlCmd | KeyCode.KEY_A, handler: (accessor) => { @@ -450,7 +452,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.toggleExpand', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, primary: KeyCode.Space, handler: (accessor) => { @@ -470,14 +472,31 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.clear', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: WorkbenchListFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(WorkbenchListFocusContextKey, WorkbenchListHasSelectionOrFocus), primary: KeyCode.Escape, handler: (accessor) => { const focused = accessor.get(IListService).lastFocusedList; - // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { + // List + if (focused instanceof List || focused instanceof PagedList) { + const list = focused; + + if (list.getSelection().length > 0) { + list.setSelection([]); + + return void 0; + } + + if (list.getFocus().length > 0) { + list.setFocus([]); + + return void 0; + } + } + + // Tree + else if (focused) { const tree = focused; if (tree.getSelection().length) { @@ -499,7 +518,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.closeWindow', // close the window when the last editor is closed by reusing the same keybinding - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(NoEditorsVisibleContext, SingleEditorGroupsContext), primary: KeyMod.CtrlCmd | KeyCode.KEY_W, handler: accessor => { @@ -510,7 +529,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.exitZenMode', - weight: KeybindingsRegistry.WEIGHT.editorContrib(-1000), + weight: KeybindingWeight.EditorContrib - 1000, handler(accessor: ServicesAccessor, configurationOrName: any) { const partService = accessor.get(IPartService); partService.toggleZenMode(); @@ -520,8 +539,8 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.quit', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + id: QUIT_ID, + weight: KeybindingWeight.WorkbenchContrib, handler(accessor: ServicesAccessor) { const windowsService = accessor.get(IWindowsService); windowsService.quit(); @@ -531,7 +550,7 @@ export function registerCommands(): void { win: { primary: void 0 } }); - CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: string) { + CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, path: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string) { const windowsService = accessor.get(IWindowsService); return windowsService.removeFromRecentlyOpened([path]).then(() => void 0); diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index a75f5da965b..355f6c69a27 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -14,14 +14,16 @@ 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, ShowStartupPerformance, ToggleSharedProcessAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, ShowAboutDialogAction, InspectContextKeysAction, OpenProcessExplorer } from 'vs/workbench/electron-browser/actions'; -import { registerCommands } from 'vs/workbench/electron-browser/commands'; +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 } 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 { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +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 { ADD_ROOT_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; +import { FileDialogContext, IsMacContext } from 'vs/platform/workbench/common/contextkeys'; // Contribute Commands registerCommands(); @@ -37,11 +39,12 @@ workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(Switch workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(QuickSwitchWindow, QuickSwitchWindow.ID, QuickSwitchWindow.LABEL), 'Quick Switch Window...'); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenRecentAction, QuickOpenRecentAction.ID, QuickOpenRecentAction.LABEL), 'File: Quick Open Recent...', fileCategory); +const isLocal = FileDialogContext.isEqualTo('local'); if (isMacintosh) { - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open...', fileCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, isLocal), 'File: Open...', fileCategory, isLocal); } else { - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileAction, OpenFileAction.ID, OpenFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open File...', fileCategory); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }), 'File: Open Folder...', fileCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileAction, OpenFileAction.ID, OpenFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, isLocal), 'File: Open File...', fileCategory, isLocal); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }, isLocal), 'File: Open Folder...', fileCategory, isLocal); } workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'File: Close Workspace', fileCategory); @@ -66,6 +69,10 @@ if (OpenTipsAndTricksUrlAction.AVAILABLE) { workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenTipsAndTricksUrlAction, OpenTipsAndTricksUrlAction.ID, OpenTipsAndTricksUrlAction.LABEL), 'Help: Tips and Tricks', helpCategory); } +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenTwitterUrlAction, OpenTwitterUrlAction.ID, OpenTwitterUrlAction.LABEL), 'Help: Join us on Twitter', helpCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenRequestFeatureUrlAction, OpenRequestFeatureUrlAction.ID, OpenRequestFeatureUrlAction.LABEL), 'Help: Search Feature Requests', helpCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLicenseUrlAction, OpenLicenseUrlAction.ID, OpenLicenseUrlAction.LABEL), 'Help: View License', helpCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenPrivacyStatementUrlAction, OpenPrivacyStatementUrlAction.ID, OpenPrivacyStatementUrlAction.LABEL), 'Help: Privacy Statement', helpCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowAboutDialogAction, ShowAboutDialogAction.ID, ShowAboutDialogAction.LABEL), 'Help: About', helpCategory); workbenchActionsRegistry.registerWorkbenchAction( @@ -101,15 +108,11 @@ workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(Increa workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(DecreaseViewSizeAction, DecreaseViewSizeAction.ID, DecreaseViewSizeAction.LABEL, null), '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); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory, isLocal); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SaveWorkspaceAsAction, SaveWorkspaceAsAction.ID, SaveWorkspaceAsAction.LABEL), 'Workspaces: Save Workspace As...', workspacesCategory); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory, isLocal); +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SaveWorkspaceAsAction, SaveWorkspaceAsAction.ID, SaveWorkspaceAsAction.LABEL), 'Workspaces: Save Workspace As...', workspacesCategory, isLocal); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(DuplicateWorkspaceInNewWindowAction, DuplicateWorkspaceInNewWindowAction.ID, DuplicateWorkspaceInNewWindowAction.LABEL), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory); -// Support old command id -CommandsRegistry.registerCommand('workbench.action.openFolderAsWorkspaceInNewWindow', serviceAccesor => { - serviceAccesor.get(IInstantiationService).createInstance(DuplicateWorkspaceInNewWindowAction, DuplicateWorkspaceInNewWindowAction.ID, DuplicateWorkspaceInNewWindowAction.LABEL).run(); -}); CommandsRegistry.registerCommand(OpenWorkspaceConfigFileAction.ID, serviceAccessor => { serviceAccessor.get(IInstantiationService).createInstance(OpenWorkspaceConfigFileAction, OpenWorkspaceConfigFileAction.ID, OpenWorkspaceConfigFileAction.LABEL).run(); @@ -124,7 +127,6 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { // Developer related actions const developerCategory = nls.localize('developer', "Developer"); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowStartupPerformance, ShowStartupPerformance.ID, ShowStartupPerformance.LABEL), 'Developer: Startup Performance', developerCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSharedProcessAction, ToggleSharedProcessAction.ID, ToggleSharedProcessAction.LABEL), 'Developer: Toggle Shared Process', developerCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(InspectContextKeysAction, InspectContextKeysAction.ID, InspectContextKeysAction.LABEL), 'Developer: Inspect Context Keys', developerCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenProcessExplorer, OpenProcessExplorer.ID, OpenProcessExplorer.LABEL), 'Developer: Open Process Explorer', developerCategory); @@ -134,7 +136,7 @@ const recentFilesPickerContext = ContextKeyExpr.and(inQuickOpenContext, ContextK const quickOpenNavigateNextInRecentFilesPickerId = 'workbench.action.quickOpenNavigateNextInRecentFilesPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigateNextInRecentFilesPickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigateNextInRecentFilesPickerId, true), when: recentFilesPickerContext, primary: KeyMod.CtrlCmd | KeyCode.KEY_R, @@ -144,13 +146,330 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const quickOpenNavigatePreviousInRecentFilesPicker = 'workbench.action.quickOpenNavigatePreviousInRecentFilesPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigatePreviousInRecentFilesPicker, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigatePreviousInRecentFilesPicker, false), when: recentFilesPickerContext, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R } }); +// Menu registration - file menu + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: NewWindowAction.ID, + title: nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenFileAction.ID, + title: nls.localize({ key: 'miOpenFile', comment: ['&& denotes a mnemonic'] }, "&&Open File...") + }, + order: 1, + when: ContextKeyExpr.and(IsMacContext.toNegated(), isLocal) +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenFolderAction.ID, + title: nls.localize({ key: 'miOpenFolder', comment: ['&& denotes a mnemonic'] }, "Open &&Folder...") + }, + order: 2, + when: ContextKeyExpr.and(IsMacContext.toNegated(), isLocal) +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenFileFolderAction.ID, + title: nls.localize({ key: 'miOpen', comment: ['&& denotes a mnemonic'] }, "&&Open...") + }, + order: 1, + when: ContextKeyExpr.and(IsMacContext, isLocal) +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: OpenWorkspaceAction.ID, + title: nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...") + }, + order: 3, + when: isLocal +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"), + submenu: MenuId.MenubarRecentMenu, + group: '2_open', + order: 4 +}); + + +// More +MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'y_more', + command: { + id: OpenRecentAction.ID, + title: nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: ADD_ROOT_FOLDER_COMMAND_ID, + title: nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "A&&dd Folder to Workspace...") + }, + order: 1, + when: isLocal +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: SaveWorkspaceAsAction.ID, + title: nls.localize('miSaveWorkspaceAs', "Save Workspace As...") + }, + order: 2, + when: isLocal +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences"), + submenu: MenuId.MenubarPreferencesMenu, + group: '5_autosave', + order: 2, + when: IsMacContext.toNegated() +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CloseWorkspaceAction.ID, + title: nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder"), + precondition: new RawContextKey('workspaceFolderCount', 0).notEqualsTo('0') + }, + order: 3, + when: new RawContextKey('workbenchState', '').notEqualsTo('workspace') +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CloseWorkspaceAction.ID, + title: nls.localize({ key: 'miCloseWorkspace', comment: ['&& denotes a mnemonic'] }, "Close &&Workspace") + }, + order: 3, + when: new RawContextKey('workbenchState', '').isEqualTo('workspace') +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CloseCurrentWindowAction.ID, + title: nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: 'z_Exit', + command: { + id: QUIT_ID, + title: nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit") + }, + order: 1, + when: IsMacContext.toNegated() +}); + +// Appereance menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_appearance', + title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), + submenu: MenuId.MenubarAppearanceMenu, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '1_toggle_view', + command: { + id: ToggleFullScreenAction.ID, + title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") + }, + 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") + }, + order: 4 +}); + +// Zoom + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomInAction.ID, + title: nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomOutAction.ID, + title: nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_zoom', + command: { + id: ZoomResetAction.ID, + title: nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") + }, + order: 3 +}); + +// Help + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.openDocumentationUrl', + title: nls.localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'update.showCurrentReleaseNotes', + title: nls.localize({ key: 'miReleaseNotes', comment: ['&& denotes a mnemonic'] }, "&&Release Notes") + }, + order: 4 +}); + +// Reference +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.keybindingsReference', + title: nls.localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.openIntroductoryVideosUrl', + title: nls.localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.openTipsAndTricksUrl', + title: nls.localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "&&Tips and Tricks") + }, + order: 3 +}); + +// Feedback +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'workbench.action.openTwitterUrl', + title: nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'workbench.action.openRequestFeatureUrl', + title: nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'workbench.action.openIssueReporter', + title: nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") + }, + order: 3 +}); + +// Legal +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '4_legal', + command: { + id: 'workbench.action.openLicenseUrl', + title: nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '4_legal', + command: { + id: 'workbench.action.openPrivacyStatementUrl', + title: nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement") + }, + order: 2 +}); + +// Tools +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.toggleDevTools', + title: nls.localize({ key: 'miToggleDevTools', comment: ['&& denotes a mnemonic'] }, "&&Toggle Developer Tools") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.openProcessExplorer', + title: nls.localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 +}); + +// About +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: 'z_about', + command: { + id: 'workbench.action.showAboutDialog', + title: nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About") + }, + order: 1, + when: IsMacContext.toNegated() +}); + // Configuration: Workbench const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -162,7 +481,7 @@ configurationRegistry.registerConfiguration({ 'properties': { 'workbench.editor.showTabs': { 'type': 'boolean', - 'description': nls.localize('showEditorTabs', "Controls if opened editors should show in tabs or not."), + 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), 'default': true }, 'workbench.editor.labelFormat': { @@ -175,52 +494,58 @@ configurationRegistry.registerConfiguration({ nls.localize('workbench.editor.labelFormat.long', "Show the name of the file followed by it's 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. Changing this setting can for example make it easier to understand the location of a file:\n- short: 'parent'\n- medium: 'workspace/src/parent'\n- long: '/home/user/workspace/src/parent'\n- default: '.../parent', when another tab shares the same title, or the relative workspace path if tabs are disabled"), + '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'.") + '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', - '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. Set to 'fit' to keep tabs always large enough to show the full editor label. Set to 'shrink' to allow tabs to get smaller when the available space is not enough to show all tabs at once.") + '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.showIcons': { 'type': 'boolean', - 'description': nls.localize('showIcons', "Controls if opened editors should show with an icon or not. This requires an icon theme to be enabled as well."), + '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 if opened editors show as preview. Preview editors are reused until they are kept (e.g. via double click or editing) and show up with an italic font style."), + 'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are kept (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 if opened editors from Quick Open show as preview. Preview editors are reused until they are kept (e.g. via double click or editing)."), + 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether opened editors from Quick Open show as preview. Preview editors are reused until they are kept (e.g. via double click or editing)."), 'default': true }, 'workbench.editor.closeOnFileDelete': { 'type': 'boolean', - 'description': nls.localize('closeOnFileDelete', "Controls if editors showing a file should close automatically when the file is deleted or renamed by some other process. Disabling this will keep the editor open as dirty 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': true + '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', - 'description': 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.") + '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', - 'description': 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 rigth hand side of the currently active one. If changed to open down, the editors will open below the currently active one.") + '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', @@ -229,7 +554,7 @@ configurationRegistry.registerConfiguration({ }, 'workbench.editor.revealIfOpen': { 'type': 'boolean', - 'description': nls.localize('revealIfOpen', "Controls if 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."), + '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': { @@ -245,17 +570,22 @@ configurationRegistry.registerConfiguration({ }, 'workbench.commandPalette.preserveInput': { 'type': 'boolean', - 'description': nls.localize('preserveInput', "Controls if the last typed input to the command palette should be restored when opening it the next time."), + '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 if Quick Open should close automatically once it loses focus."), + 'description': nls.localize('closeOnFocusLost', "Controls whether Quick Open should close automatically once it loses focus."), 'default': true }, 'workbench.settings.openDefaultSettings': { 'type': 'boolean', - 'description': nls.localize('openDefaultSettings', "Controls if opening settings also opens an editor showing all default settings."), + 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), + 'default': true + }, + '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': { @@ -290,7 +620,7 @@ configurationRegistry.registerConfiguration({ 'enum': ['default', 'antialiased', 'none', 'auto'], 'default': 'default', 'description': - nls.localize('fontAliasing', "Controls font aliasing method in the workbench.\n- default: Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text\n- antialiased: Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall\n- none: Disables font smoothing. Text will show with jagged sharp edges\n- auto: Applies `default` or `antialiased` automatically based on the DPI of displays."), + 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."), @@ -301,8 +631,38 @@ configurationRegistry.registerConfiguration({ }, 'workbench.settings.enableNaturalLanguageSearch': { 'type': 'boolean', - 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings."), - 'default': true + 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings. The natural language search is provided by an 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'] } } }); @@ -319,41 +679,41 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': ['on', 'off', 'default'], 'enumDescriptions': [ - nls.localize('window.openFilesInNewWindow.on', "Files will open in a new window"), - nls.localize('window.openFilesInNewWindow.off', "Files will open in the window with the files' folder open or the last active window"), + nls.localize('window.openFilesInNewWindow.on', "Files will open in a new window."), + nls.localize('window.openFilesInNewWindow.off', "Files will open in the window with the files' folder open or the last active window."), isMacintosh ? - nls.localize('window.openFilesInNewWindow.defaultMac', "Files will open in the window with the files' folder open or the last active window unless opened via the Dock or from Finder") : - nls.localize('window.openFilesInNewWindow.default', "Files will open in a new window unless picked from within the application (e.g. via the File menu)") + nls.localize('window.openFilesInNewWindow.defaultMac', "Files will open in the window with the files' folder open or the last active window unless opened via the Dock or from Finder.") : + nls.localize('window.openFilesInNewWindow.default', "Files will open in a new window unless picked from within the application (e.g. via the File menu).") ], 'default': 'off', 'scope': ConfigurationScope.APPLICATION, - 'description': + 'markdownDescription': isMacintosh ? - nls.localize('openFilesInNewWindowMac', "Controls if files should open in a new window.\n- default: files will open in the window with the files' folder open or the last active window unless opened via the Dock or from Finder\n- on: files will open in a new window\n- off: files will open in the window with the files' folder open or the last active window\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") : - nls.localize('openFilesInNewWindow', "Controls if files should open in a new window.\n- default: files will open in a new window unless picked from within the application (e.g. via the File menu)\n- on: files will open in a new window\n- off: files will open in the window with the files' folder open or the last active window\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") + nls.localize('openFilesInNewWindowMac', "Controls whether files should open in a new window. \nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).") : + nls.localize('openFilesInNewWindow', "Controls whether files should open in a new window.\nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).") }, 'window.openFoldersInNewWindow': { 'type': 'string', 'enum': ['on', 'off', 'default'], 'enumDescriptions': [ - nls.localize('window.openFoldersInNewWindow.on', "Folders will open in a new window"), - nls.localize('window.openFoldersInNewWindow.off', "Folders will replace the last active window"), - nls.localize('window.openFoldersInNewWindow.default', "Folders will open in a new window unless a folder is picked from within the application (e.g. via the File menu)") + nls.localize('window.openFoldersInNewWindow.on', "Folders will open in a new window."), + nls.localize('window.openFoldersInNewWindow.off', "Folders will replace the last active window."), + nls.localize('window.openFoldersInNewWindow.default', "Folders will open in a new window unless a folder is picked from within the application (e.g. via the File menu).") ], 'default': 'default', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('openFoldersInNewWindow', "Controls if folders should open in a new window or replace the last active window.\n- default: folders will open in a new window unless a folder is picked from within the application (e.g. via the File menu)\n- on: folders will open in a new window\n- off: folders will replace the last active window\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") + 'markdownDescription': nls.localize('openFoldersInNewWindow', "Controls whether folders should open in a new window or replace the last active window.\nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).") }, 'window.openWithoutArgumentsInNewWindow': { 'type': 'string', 'enum': ['on', 'off'], 'enumDescriptions': [ - nls.localize('window.openWithoutArgumentsInNewWindow.on', "Open a new empty window"), - nls.localize('window.openWithoutArgumentsInNewWindow.off', "Focus the last active running instance") + nls.localize('window.openWithoutArgumentsInNewWindow.on', "Open a new empty window."), + nls.localize('window.openWithoutArgumentsInNewWindow.off', "Focus the last active running instance.") ], 'default': isMacintosh ? 'off' : 'on', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('openWithoutArgumentsInNewWindow', "Controls if a new empty window should open when starting a second instance without arguments or if the last running instance should get focus.\n- on: open a new empty window\n- off: the last active running instance will get focus\nNote that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).") + 'markdownDescription': nls.localize('openWithoutArgumentsInNewWindow', "Controls whether a new empty window should open when starting a second instance without arguments or if the last running instance should get focus.\nNote that there can still be cases where this setting is ignored (e.g. when using the `--new-window` or `--reuse-window` command line option).") }, 'window.restoreWindows': { 'type': 'string', @@ -366,13 +726,13 @@ configurationRegistry.registerConfiguration({ ], 'default': 'one', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('restoreWindows', "Controls how windows are being reopened after a restart. Select 'none' to always start with an empty workspace, 'one' to reopen the last window you worked on, 'folders' to reopen all windows that had folders opened or 'all' to reopen all windows of your last session.") + 'description': nls.localize('restoreWindows', "Controls how windows are being reopened after a restart.") }, 'window.restoreFullscreen': { 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('restoreFullscreen', "Controls if a window should restore to full screen mode if it was exited in full screen mode.") + 'description': nls.localize('restoreFullscreen', "Controls whether a window should restore to full screen mode if it was exited in full screen mode.") }, 'window.zoomLevel': { 'type': 'number', @@ -382,8 +742,8 @@ configurationRegistry.registerConfiguration({ 'window.title': { 'type': 'string', 'default': isMacintosh ? '${activeEditorShort}${separator}${rootName}' : '${dirty}${activeEditorShort}${separator}${rootName}${separator}${appName}', - 'description': 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") + '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.") }, 'window.newWindowDimensions': { 'type': 'string', @@ -396,12 +756,12 @@ configurationRegistry.registerConfiguration({ ], 'default': 'default', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('newWindowDimensions', "Controls the dimensions of opening a new window when at least one window is already opened. By default, a new window will open in the center of the screen with small dimensions. When set to 'inherit', the window will get the same dimensions as the last window that was active. When set to 'maximized', the window will open maximized and fullscreen if configured to 'fullscreen'. Note that this setting does not have an impact on the first window that is opened. The first window will always restore the size and location as you left it before closing.") + 'description': nls.localize('newWindowDimensions', "Controls the dimensions of opening a new window when at least one window is already opened. Note that this setting does not have an impact on the first window that is opened. The first window will always restore the size and location as you left it before closing.") }, 'window.closeWhenEmpty': { 'type': 'boolean', 'default': false, - 'description': nls.localize('closeWhenEmpty', "Controls if closing the last editor should also close the window. This setting only applies for windows that do not show folders.") + 'description': nls.localize('closeWhenEmpty', "Controls whether closing the last editor should also close the window. This setting only applies for windows that do not show folders.") }, 'window.menuBarVisibility': { 'type': 'string', @@ -433,10 +793,9 @@ configurationRegistry.registerConfiguration({ 'window.titleBarStyle': { 'type': 'string', 'enum': ['native', 'custom'], - 'default': 'custom', + 'default': isLinux ? 'native' : 'custom', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('titleBarStyle', "Adjust the appearance of the window title bar. Changes require a full restart to apply."), - 'included': isMacintosh + 'description': nls.localize('titleBarStyle', "Adjust the appearance of the window title bar. Changes require a full restart to apply.") }, 'window.nativeTabs': { 'type': 'boolean', @@ -449,7 +808,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'description': 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."), + '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. Note: in order for this workaround to function, make sure to also set `#window.titleBarStyle#` to `native`."), 'included': isWindows }, 'window.clickThroughInactive': { @@ -472,32 +831,32 @@ configurationRegistry.registerConfiguration({ 'zenMode.fullScreen': { 'type': 'boolean', 'default': true, - 'description': nls.localize('zenMode.fullScreen', "Controls if turning on Zen Mode also puts the workbench into full screen mode.") + '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 if turning on Zen Mode also centers the layout.") + '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 if turning on Zen Mode also hides workbench tabs.") + '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 if turning on Zen Mode also hides the status bar at the bottom of the workbench.") + '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 if turning on Zen Mode also hides the activity bar at the left of the workbench.") + 'description': nls.localize('zenMode.hideActivityBar', "Controls whether turning on Zen Mode also hides the activity bar at the left of the workbench.") }, 'zenMode.restore': { 'type': 'boolean', 'default': false, - 'description': nls.localize('zenMode.restore', "Controls if a window should restore to zen mode if it was exited in zen mode.") + '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/main.ts b/src/vs/workbench/electron-browser/main.ts index 087bbe40d24..5323ed87b65 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -14,44 +14,48 @@ import { domContentLoaded } from 'vs/base/browser/dom'; import * as errors from 'vs/base/common/errors'; import * as comparer from 'vs/base/common/comparers'; import * as platform from 'vs/base/common/platform'; -import * as paths from 'vs/base/common/paths'; -import uri from 'vs/base/common/uri'; -import * as strings from 'vs/base/common/strings'; +import { URI as uri } from 'vs/base/common/uri'; import { IWorkspaceContextService, Workspace, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { realpath } from 'vs/base/node/pfs'; +import { stat } from 'vs/base/node/pfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import * as gracefulFs from 'graceful-fs'; -import { IInitData } from 'vs/workbench/services/timer/common/timerService'; -import { TimerService } from 'vs/workbench/services/timer/node/timerService'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows'; -import { WindowsChannelClient } from 'vs/platform/windows/common/windowsIpc'; +import { WindowsChannelClient } from 'vs/platform/windows/node/windowsIpc'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { StorageService, inMemoryLocalStorageInstance, IStorage } from 'vs/platform/storage/common/storageService'; import { Client as ElectronIPCClient } from 'vs/base/parts/ipc/electron-browser/ipc.electron-browser'; import { webFrame } from 'electron'; -import { UpdateChannelClient } from 'vs/platform/update/common/updateIpc'; +import { UpdateChannelClient } from 'vs/platform/update/node/updateIpc'; import { IUpdateService } from 'vs/platform/update/common/update'; -import { URLHandlerChannel, URLServiceChannelClient } from 'vs/platform/url/common/urlIpc'; +import { URLHandlerChannel, URLServiceChannelClient } from 'vs/platform/url/node/urlIpc'; import { IURLService } from 'vs/platform/url/common/url'; -import { WorkspacesChannelClient } from 'vs/platform/workspaces/common/workspacesIpc'; -import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { WorkspacesChannelClient } from 'vs/platform/workspaces/node/workspacesIpc'; +import { IWorkspacesService, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import * as fs from 'fs'; import { ConsoleLogService, MultiplexLogService, ILogService } from 'vs/platform/log/common/log'; -import { IssueChannelClient } from 'vs/platform/issue/common/issueIpc'; +import { IssueChannelClient } from 'vs/platform/issue/node/issueIpc'; import { IIssueService } from 'vs/platform/issue/common/issue'; -import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; +import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/node/logIpc'; import { RelayURLService } from 'vs/platform/url/common/urlService'; +import { MenubarChannelClient } from 'vs/platform/menubar/node/menubarIpc'; +import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { Schemas } from 'vs/base/common/network'; +import { sanitizeFilePath } from 'vs/base/node/extfs'; gracefulFs.gracefulify(fs); // enable gracefulFs export function startup(configuration: IWindowConfiguration): TPromise { + revive(configuration); + + perf.importEntries(configuration.perfEntries); + // Ensure others can listen to zoom level changes browser.setZoomFactor(webFrame.getZoomFactor()); @@ -72,6 +76,22 @@ export function startup(configuration: IWindowConfiguration): TPromise { return openWorkbench(configuration); } +function revive(workbench: IWindowConfiguration) { + if (workbench.folderUri) { + workbench.folderUri = uri.revive(workbench.folderUri); + } + const filesToWaitPaths = workbench.filesToWait && workbench.filesToWait.paths; + [filesToWaitPaths, workbench.filesToOpen, workbench.filesToCreate, workbench.filesToDiff].forEach(paths => { + if (Array.isArray(paths)) { + paths.forEach(path => { + if (path.fileUri) { + path.fileUri = uri.revive(path.fileUri); + } + }); + } + }); +} + function openWorkbench(configuration: IWindowConfiguration): TPromise { const mainProcessClient = new ElectronIPCClient(`window:${configuration.windowId}`); const mainServices = createMainProcessServices(mainProcessClient, configuration); @@ -83,7 +103,6 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { // Since the configuration service is one of the core services that is used in so many places, we initialize it // right before startup of the workbench shell to have its data ready for consumers return createAndInitializeWorkspaceService(configuration, environmentService).then(workspaceService => { - const timerService = new TimerService((window).MonacoEnvironment.timers as IInitData, workspaceService.getWorkbenchState() === WorkbenchState.EMPTY); const storageService = createStorageService(workspaceService, environmentService); return domContentLoaded().then(() => { @@ -95,7 +114,6 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { configurationService: workspaceService, environmentService, logService, - timerService, storageService }, mainServices, mainProcessClient, configuration); shell.open(); @@ -104,7 +122,7 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { (self).require.config({ onError: (err: any) => { if (err.errorCode === 'load') { - shell.onUnexpectedError(loaderError(err)); + shell.onUnexpectedError(new Error(nls.localize('loaderErrorNative', "Failed to load a required file. Please restart the application to try again. Details: {0}", JSON.stringify(err)))); } } }); @@ -113,44 +131,30 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { } function createAndInitializeWorkspaceService(configuration: IWindowConfiguration, environmentService: EnvironmentService): TPromise { - return validateSingleFolderPath(configuration).then(() => { + return validateFolderUri(configuration.folderUri, configuration.verbose).then(validatedFolderUri => { + const workspaceService = new WorkspaceService(environmentService); - return workspaceService.initialize(configuration.workspace || configuration.folderPath || configuration).then(() => workspaceService, error => workspaceService); + return workspaceService.initialize(configuration.workspace || validatedFolderUri || configuration).then(() => workspaceService, error => workspaceService); }); } -function validateSingleFolderPath(configuration: IWindowConfiguration): TPromise { +function validateFolderUri(folderUri: ISingleFolderWorkspaceIdentifier, verbose: boolean): TPromise { - // Return early if we do not have a single folder path - if (!configuration.folderPath) { - return TPromise.as(void 0); + // Return early if we do not have a single folder uri or if it is a non file uri + if (!folderUri || folderUri.scheme !== Schemas.file) { + return TPromise.as(folderUri); } - // Otherwise: use realpath to resolve symbolic links to the truth - return realpath(configuration.folderPath).then(realFolderPath => { - - // For some weird reason, node adds a trailing slash to UNC paths - // we never ever want trailing slashes as our workspace path unless - // someone opens root ("/"). - // See also https://github.com/nodejs/io.js/issues/1765 - if (paths.isUNC(realFolderPath) && strings.endsWith(realFolderPath, paths.nativeSep)) { - realFolderPath = strings.rtrim(realFolderPath, paths.nativeSep); - } - - return realFolderPath; - }, error => { - if (configuration.verbose) { + // Ensure absolute existing folder path + const sanitizedFolderPath = sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd()); + return stat(sanitizedFolderPath).then(stat => uri.file(sanitizedFolderPath), error => { + if (verbose) { errors.onUnexpectedError(error); } // Treat any error case as empty workbench case (no folder path) return null; - - }).then(realFolderPathOrNull => { - - // Update config with real path if we have one - configuration.folderPath = realFolderPathOrNull; }); } @@ -190,10 +194,7 @@ function createStorageService(workspaceService: IWorkspaceContextService, enviro if (disableStorage) { storage = inMemoryLocalStorageInstance; } else { - // TODO@Ben remove me after a while - perf.mark('willAccessLocalStorage'); storage = window.localStorage; - perf.mark('didAccessLocalStorage'); } return new StorageService(storage, storage, workspaceId, secondaryWorkspaceId); @@ -227,16 +228,11 @@ function createMainProcessServices(mainProcessClient: ElectronIPCClient, configu const issueChannel = mainProcessClient.getChannel('issue'); serviceCollection.set(IIssueService, new SyncDescriptor(IssueChannelClient, issueChannel)); + const menubarChannel = mainProcessClient.getChannel('menubar'); + serviceCollection.set(IMenubarService, new SyncDescriptor(MenubarChannelClient, menubarChannel)); + const workspacesChannel = mainProcessClient.getChannel('workspaces'); serviceCollection.set(IWorkspacesService, new WorkspacesChannelClient(workspacesChannel)); return serviceCollection; -} - -function loaderError(err: Error): Error { - if (platform.isWeb) { - return new Error(nls.localize('loaderError', "Failed to load a required file. Either you are no longer connected to the internet or the server you are connected to is offline. Please refresh the browser to try again.")); - } - - return new Error(nls.localize('loaderErrorNative', "Failed to load a required file. Please restart the application to try again. Details: {0}", JSON.stringify(err))); -} +} \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/media/shell.css b/src/vs/workbench/electron-browser/media/shell.css index 2d3ce109261..306a4b4e6db 100644 --- a/src/vs/workbench/electron-browser/media/shell.css +++ b/src/vs/workbench/electron-browser/media/shell.css @@ -15,11 +15,16 @@ /* Font Families (with CJK support) */ -.monaco-shell { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } -.monaco-shell:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } -.monaco-shell:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } -.monaco-shell:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Meiryo", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } -.monaco-shell:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +.monaco-shell, +.monaco-shell .monaco-menu-container .monaco-menu { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } +.monaco-shell:lang(zh-Hans), +.monaco-shell:lang(zh-Hans) .monaco-menu-container .monaco-menu { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.monaco-shell:lang(zh-Hant), +.monaco-shell:lang(zh-Hant) .monaco-menu-container .monaco-menu { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } +.monaco-shell:lang(ja), +.monaco-shell:lang(ja) .monaco-menu-container .monaco-menu { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Meiryo", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } +.monaco-shell:lang(ko), +.monaco-shell:lang(ko) .monaco-menu-container .monaco-menu { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @@ -65,6 +70,40 @@ cursor: pointer; } +.monaco-shell .monaco-menu .monaco-action-bar.vertical { + padding: .5em 0; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-menu-item { + height: 1.8em; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), +.monaco-shell .monaco-menu .monaco-action-bar.vertical .keybinding { + font-size: inherit; + padding: 0 2em; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .menu-item-check { + font-size: inherit; + width: 2em; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label.separator { + font-size: inherit; + padding: 0.2em 0 0 0; + margin-bottom: 0.2em; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .submenu-indicator { + font-size: 60%; + padding: 0 1.8em; +} + +.monaco-shell .monaco-menu .action-item { + cursor: default; +} + /* START Keyboard Focus Indication Styles */ .monaco-shell [tabindex="0"]:focus, @@ -85,7 +124,6 @@ .monaco-shell input[type="button"]:active, .monaco-shell input[type="checkbox"]:active, .monaco-shell .monaco-tree .monaco-tree-row -.monaco-action-bar .action-item [tabindex="0"]:hover, .monaco-shell .monaco-tree.focused.no-focused-item:active:before { outline: 0 !important; /* fixes some flashing outlines from showing up when clicking */ } @@ -94,8 +132,6 @@ border: none; /* outline is a square, but border has a radius, so we avoid this glitch when focused (https://github.com/Microsoft/vscode/issues/26045) */ } - - .monaco-shell .monaco-tree.focused .monaco-tree-row.focused [tabindex="0"]:focus { outline-width: 1px; /* higher contrast color for focusable elements in a row that shows focus feedback */ outline-style: solid; diff --git a/src/vs/workbench/electron-browser/media/workbench.css b/src/vs/workbench/electron-browser/media/workbench.css index 0968fc3040e..28c4a57c000 100644 --- a/src/vs/workbench/electron-browser/media/workbench.css +++ b/src/vs/workbench/electron-browser/media/workbench.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench-container { - position: absolute; -} - .monaco-workbench { font-size: 13px; line-height: 1.4em; diff --git a/src/vs/workbench/electron-browser/resources.ts b/src/vs/workbench/electron-browser/resources.ts index fce07d4794d..fe28a4b3267 100644 --- a/src/vs/workbench/electron-browser/resources.ts +++ b/src/vs/workbench/electron-browser/resources.ts @@ -5,22 +5,23 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as objects from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { relative } from 'path'; import { normalize } from 'vs/base/common/paths'; -export class ResourceGlobMatcher { +export class ResourceGlobMatcher extends Disposable { private static readonly NO_ROOT: string = null; - private readonly _onExpressionChange: Emitter; - private toUnbind: IDisposable[]; + private readonly _onExpressionChange: Emitter = this._register(new Emitter()); + get onExpressionChange(): Event { return this._onExpressionChange.event; } + private mapRootToParsedExpression: Map; private mapRootToExpressionConfig: Map; @@ -30,30 +31,24 @@ export class ResourceGlobMatcher { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService ) { - this.toUnbind = []; + super(); this.mapRootToParsedExpression = new Map(); this.mapRootToExpressionConfig = new Map(); - this._onExpressionChange = new Emitter(); - this.toUnbind.push(this._onExpressionChange); - this.updateExcludes(false); this.registerListeners(); } - public get onExpressionChange(): Event { - return this._onExpressionChange.event; - } - private registerListeners(): void { - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (this.shouldUpdate(e)) { this.updateExcludes(true); } })); - this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateExcludes(true))); + + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.updateExcludes(true))); } private updateExcludes(fromEvent: boolean): void { @@ -98,7 +93,7 @@ export class ResourceGlobMatcher { } } - public matches(resource: URI): boolean { + matches(resource: URI): boolean { const folder = this.contextService.getWorkspaceFolder(resource); let expressionForRoot: ParsedExpression; @@ -121,8 +116,4 @@ export class ResourceGlobMatcher { return !!expressionForRoot(resourcePathToMatch); } - - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); - } } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 975def9b462..4f87c195ef7 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -10,18 +10,16 @@ import 'vs/css!./media/shell'; import * as platform from 'vs/base/common/platform'; import * as perf from 'vs/base/common/performance'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as errors from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import product from 'vs/platform/node/product'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import pkg from 'vs/platform/node/package'; -import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { Workbench, IWorkbenchStartedInfo } from 'vs/workbench/electron-browser/workbench'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService, configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IExperimentService, ExperimentService } from 'vs/platform/telemetry/common/experiments'; -import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; +import { NullTelemetryService, configurationTelemetry, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { ElectronWindow } from 'vs/workbench/electron-browser/window'; @@ -47,7 +45,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ILifecycleService, LifecyclePhase, ShutdownReason, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -60,12 +57,11 @@ import { WorkbenchModeServiceImpl } from 'vs/workbench/services/mode/common/work import { IModeService } from 'vs/editor/common/services/modeService'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { ICrashReporterService, NullCrashReporterService, CrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService'; -import { getDelayedChannel, IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { getDelayedChannel, IPCClient } from 'vs/base/parts/ipc/node/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IExtensionManagementChannel, ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; -import { IExtensionManagementService, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementChannel, ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/node/extensionManagementIpc'; +import { IExtensionManagementService, IExtensionEnablementService, IExtensionManagementServerService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { ITimerService } from 'vs/workbench/services/timer/common/timerService'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { restoreFontInfo, readFontInfo, saveFontInfo } from 'vs/editor/browser/config/configuration'; import * as browser from 'vs/base/browser/browser'; @@ -84,7 +80,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { stat } from 'fs'; import { join } from 'path'; -import { ILocalizationsChannel, LocalizationsChannelClient } from 'vs/platform/localizations/common/localizationsIpc'; +import { ILocalizationsChannel, LocalizationsChannelClient } from 'vs/platform/localizations/node/localizationsIpc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; import { WorkbenchIssueService } from 'vs/workbench/services/issue/electron-browser/workbenchIssueService'; @@ -92,11 +88,17 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { NotificationService } from 'vs/workbench/services/notification/common/notificationService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; -import { DialogChannel } from 'vs/platform/dialogs/common/dialogIpc'; -import { EventType, addDisposableListener, addClass, getClientArea } from 'vs/base/browser/dom'; +import { DialogChannel } from 'vs/platform/dialogs/node/dialogIpc'; +import { EventType, addDisposableListener, addClass } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { SearchHistoryService } from 'vs/workbench/services/search/node/searchHistoryService'; +import { MulitExtensionManagementService } from 'vs/platform/extensionManagement/node/multiExtensionManagement'; +import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { DownloadServiceChannel } from 'vs/platform/download/node/downloadIpc'; +import { DefaultURITransformer } from 'vs/base/common/uriIpc'; +import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; +import { ILabelService } from 'vs/platform/label/common/label'; /** * Services that we require for the Shell @@ -106,7 +108,6 @@ export interface ICoreServices { configurationService: IConfigurationService; environmentService: IEnvironmentService; logService: ILogService; - timerService: ITimerService; storageService: IStorageService; } @@ -114,34 +115,31 @@ 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 { +export class WorkbenchShell extends Disposable { private storageService: IStorageService; private environmentService: IEnvironmentService; + private labelService: ILabelService; private logService: ILogService; - private contextViewService: ContextViewService; private configurationService: IConfigurationService; private contextService: IWorkspaceContextService; private telemetryService: ITelemetryService; - private experimentService: IExperimentService; private extensionService: ExtensionService; private broadcastService: IBroadcastService; - private timerService: ITimerService; private themeService: WorkbenchThemeService; private lifecycleService: LifecycleService; private mainProcessServices: ServiceCollection; private notificationService: INotificationService; private container: HTMLElement; - private toUnbind: IDisposable[]; private previousErrorValue: string; private previousErrorTime: number; - private content: HTMLElement; - private contentsContainer: HTMLElement; private configuration: IWindowConfiguration; private workbench: Workbench; constructor(container: HTMLElement, coreServices: ICoreServices, mainProcessServices: ServiceCollection, private mainProcessClient: IPCClient, configuration: IWindowConfiguration) { + super(); + this.container = container; this.configuration = configuration; @@ -150,32 +148,26 @@ export class WorkbenchShell { this.configurationService = coreServices.configurationService; this.environmentService = coreServices.environmentService; this.logService = coreServices.logService; - this.timerService = coreServices.timerService; this.storageService = coreServices.storageService; this.mainProcessServices = mainProcessServices; - this.toUnbind = []; this.previousErrorTime = 0; } - private createContents(parent: HTMLElement): HTMLElement { + private renderContents(): void { // ARIA aria.setARIAContainer(document.body); - // Workbench Container - const workbenchContainer = document.createElement('div'); - parent.appendChild(workbenchContainer); - // Instantiation service with services - const [instantiationService, serviceCollection] = this.initServiceCollection(parent); + const [instantiationService, serviceCollection] = this.initServiceCollection(this.container); // Workbench - this.workbench = this.createWorkbench(instantiationService, serviceCollection, parent, workbenchContainer); + this.workbench = this.createWorkbench(instantiationService, serviceCollection, this.container); // Window - this.workbench.getInstantiationService().createInstance(ElectronWindow, this.container); + this.workbench.getInstantiationService().createInstance(ElectronWindow); // Handle case where workbench is not starting up properly const timeoutHandle = setTimeout(() => { @@ -185,19 +177,27 @@ export class WorkbenchShell { this.lifecycleService.when(LifecyclePhase.Running).then(() => { clearTimeout(timeoutHandle); }); - - return workbenchContainer; } - private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, parent: HTMLElement, workbenchContainer: HTMLElement): Workbench { + private createWorkbench(instantiationService: IInstantiationService, serviceCollection: ServiceCollection, container: HTMLElement): Workbench { + + function handleStartupError(logService: ILogService, error: Error): void { + + // Log it + logService.error(toErrorMessage(error, true)); + + // Rethrow + throw error; + } + try { - const workbench = instantiationService.createInstance(Workbench, parent, workbenchContainer, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); + const workbench = instantiationService.createInstance(Workbench, container, this.configuration, serviceCollection, this.lifecycleService, this.mainProcessClient); // Set lifecycle phase to `Restoring` this.lifecycleService.phase = LifecyclePhase.Restoring; // Startup Workbench - workbench.startup().done(startupInfos => { + workbench.startup().then(startupInfos => { // Set lifecycle phase to `Runnning` so that other contributions can now do something this.lifecycleService.phase = LifecyclePhase.Running; @@ -210,28 +210,24 @@ export class WorkbenchShell { eventuallPhaseTimeoutHandle = void 0; this.lifecycleService.phase = LifecyclePhase.Eventually; }, 3000); - this.toUnbind.push({ - dispose: () => { - if (eventuallPhaseTimeoutHandle) { - clearTimeout(eventuallPhaseTimeoutHandle); - } + + this._register(toDisposable(() => { + if (eventuallPhaseTimeoutHandle) { + clearTimeout(eventuallPhaseTimeoutHandle); } - }); + })); // localStorage metrics (TODO@Ben remove me later) if (!this.environmentService.extensionTestsPath && this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { this.logLocalStorageMetrics(); } - }); + }, error => handleStartupError(this.logService, error)); return workbench; } catch (error) { + handleStartupError(this.logService, error); - // Log it - this.logService.error(toErrorMessage(error, true)); - - // Rethrow - throw error; + return void 0; } } @@ -251,7 +247,6 @@ export class WorkbenchShell { "customKeybindingsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "theme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "language": { "classification": "SystemMetaData", "purpose": "BusinessInsight" }, - "experiments": { "${inline}": [ "${IExperiments}" ] }, "pinnedViewlets": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "restoredViewlet": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "restoredEditors": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -269,7 +264,6 @@ export class WorkbenchShell { customKeybindingsCount: info.customKeybindingsCount, theme: this.themeService.getColorTheme().id, language: platform.language, - experiments: this.experimentService.getExperiments(), pinnedViewlets: info.pinnedViewlets, restoredViewlet: info.restoredViewlet, restoredEditors: info.restoredEditorsCount, @@ -332,10 +326,9 @@ export class WorkbenchShell { serviceCollection.set(IWorkspaceContextService, this.contextService); serviceCollection.set(IConfigurationService, this.configurationService); serviceCollection.set(IEnvironmentService, this.environmentService); - serviceCollection.set(ILogService, this.logService); - this.toUnbind.push(this.logService); + serviceCollection.set(ILabelService, this.labelService); + serviceCollection.set(ILogService, this._register(this.logService)); - serviceCollection.set(ITimerService, this.timerService); serviceCollection.set(IStorageService, this.storageService); this.mainProcessServices.forEach((serviceIdentifier, serviceInstance) => { serviceCollection.set(serviceIdentifier, serviceInstance); @@ -354,8 +347,10 @@ export class WorkbenchShell { const sharedProcess = (serviceCollection.get(IWindowsService)).whenSharedProcessReady() .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${this.configuration.windowId}`)); - sharedProcess - .done(client => client.registerChannel('dialog', instantiationService.createInstance(DialogChannel))); + sharedProcess.then(client => { + client.registerChannel('download', new DownloadServiceChannel()); + client.registerChannel('dialog', instantiationService.createInstance(DialogChannel)); + }); // Warm up font cache information before building up too many dom elements restoreFontInfo(this.storageService); @@ -364,34 +359,24 @@ export class WorkbenchShell { // Hash serviceCollection.set(IHashService, new SyncDescriptor(HashService)); - // Experiments - this.experimentService = instantiationService.createInstance(ExperimentService); - serviceCollection.set(IExperimentService, this.experimentService); - // Telemetry - if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { - const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); - const commit = product.commit; - const version = pkg.version; + if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { + const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender'))); const config: ITelemetryServiceConfig = { - appender: new TelemetryAppenderClient(channel), - commonProperties: resolveWorkbenchCommonProperties(this.storageService, commit, version, this.configuration.machineId, this.environmentService.installSourcePath), + appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService)), + commonProperties: resolveWorkbenchCommonProperties(this.storageService, product.commit, pkg.version, this.configuration.machineId, this.environmentService.installSourcePath), piiPaths: [this.environmentService.appRoot, this.environmentService.extensionsPath] }; - const telemetryService = instantiationService.createInstance(TelemetryService, config); - this.telemetryService = telemetryService; - - const errorTelemetry = new ErrorTelemetry(telemetryService); - - this.toUnbind.push(telemetryService, errorTelemetry); + this.telemetryService = this._register(instantiationService.createInstance(TelemetryService, config)); + this._register(new ErrorTelemetry(this.telemetryService)); } else { this.telemetryService = NullTelemetryService; } serviceCollection.set(ITelemetryService, this.telemetryService); - this.toUnbind.push(configurationTelemetry(this.telemetryService, this.configurationService)); + this._register(configurationTelemetry(this.telemetryService, this.configurationService)); let crashReporterService = NullCrashReporterService; if (!this.environmentService.disableCrashReporter && product.crashReporter && product.hockeyApp) { @@ -402,35 +387,33 @@ export class WorkbenchShell { serviceCollection.set(IDialogService, instantiationService.createInstance(DialogService)); const lifecycleService = instantiationService.createInstance(LifecycleService); - this.toUnbind.push(lifecycleService.onShutdown(reason => this.dispose(reason))); + this._register(lifecycleService.onShutdown(reason => this.dispose(reason))); serviceCollection.set(ILifecycleService, lifecycleService); this.lifecycleService = lifecycleService; - const extensionManagementChannel = getDelayedChannel(sharedProcess.then(c => c.getChannel('extensions'))); - serviceCollection.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementChannelClient, extensionManagementChannel)); - - const extensionEnablementService = instantiationService.createInstance(ExtensionEnablementService); - serviceCollection.set(IExtensionEnablementService, extensionEnablementService); - this.toUnbind.push(extensionEnablementService); - serviceCollection.set(IRequestService, new SyncDescriptor(RequestService)); + serviceCollection.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + + const extensionManagementChannel = getDelayedChannel(sharedProcess.then(c => c.getChannel('extensions'))); + const extensionManagementChannelClient = new ExtensionManagementChannelClient(extensionManagementChannel, DefaultURITransformer); + serviceCollection.set(IExtensionManagementServerService, new SyncDescriptor(ExtensionManagementServerService, extensionManagementChannelClient)); + serviceCollection.set(IExtensionManagementService, new SyncDescriptor(MulitExtensionManagementService)); + + const extensionEnablementService = this._register(instantiationService.createInstance(ExtensionEnablementService)); + serviceCollection.set(IExtensionEnablementService, extensionEnablementService); + this.extensionService = instantiationService.createInstance(ExtensionService); serviceCollection.set(IExtensionService, this.extensionService); perf.mark('willLoadExtensions'); - this.extensionService.whenInstalledExtensionsRegistered().done(() => { - perf.mark('didLoadExtensions'); - }); + this.extensionService.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions')); this.themeService = instantiationService.createInstance(WorkbenchThemeService, document.body); serviceCollection.set(IWorkbenchThemeService, this.themeService); serviceCollection.set(ICommandService, new SyncDescriptor(CommandService)); - this.contextViewService = instantiationService.createInstance(ContextViewService, this.container); - serviceCollection.set(IContextViewService, this.contextViewService); - serviceCollection.set(IMarkerService, new SyncDescriptor(MarkerService)); serviceCollection.set(IModeService, new SyncDescriptor(WorkbenchModeServiceImpl)); @@ -463,7 +446,17 @@ export class WorkbenchShell { return [instantiationService, serviceCollection]; } - public open(): void { + open(): void { + + // Listen on unhandled rejection events + window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + errors.onUnexpectedError(event.reason); + + // Prevent the printing of this event to the console + event.preventDefault(); + }); // Listen on unexpected errors errors.setUnexpectedErrorHandler((error: any) => { @@ -473,13 +466,8 @@ export class WorkbenchShell { // Shell Class for CSS Scoping addClass(this.container, 'monaco-shell'); - // Controls - this.content = document.createElement('div'); - addClass(this.content, 'monaco-shell-content'); - this.container.appendChild(this.content); - // Create Contents - this.contentsContainer = this.createContents(this.content); + this.renderContents(); // Layout this.layout(); @@ -491,10 +479,14 @@ export class WorkbenchShell { private registerListeners(): void { // Resize - this.toUnbind.push(addDisposableListener(window, EventType.RESIZE, () => this.layout())); + this._register(addDisposableListener(window, EventType.RESIZE, e => { + if (e.target === window) { + this.layout(); + } + })); } - public onUnexpectedError(error: any): void { + onUnexpectedError(error: any): void { const errorMsg = toErrorMessage(error, true); if (!errorMsg) { return; @@ -518,19 +510,11 @@ export class WorkbenchShell { } private layout(): void { - const clientArea = getClientArea(this.container); - - this.contentsContainer.style.width = `${clientArea.width}px`; - this.contentsContainer.style.height = `${clientArea.height}px`; - - this.contextViewService.layout(); this.workbench.layout(); } - public dispose(reason = ShutdownReason.QUIT): void { - - // Dispose bindings - this.toUnbind = dispose(this.toUnbind); + dispose(reason = ShutdownReason.QUIT): void { + super.dispose(); // Keep font info for next startup around saveFontInfo(this.storageService); @@ -542,6 +526,7 @@ export class WorkbenchShell { } } + registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { // Foreground diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 4552d8a7555..83cc52d8a47 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -6,29 +6,26 @@ 'use strict'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as errors from 'vs/base/common/errors'; -import * as types from 'vs/base/common/types'; import { TPromise } from 'vs/base/common/winjs.base'; import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, Action } from 'vs/base/common/actions'; -import { AutoSaveConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IWindowsService, IWindowService, IWindowSettings, IPath, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IPathData } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; -import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as browser from 'vs/base/browser/browser'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IResourceInput } from 'vs/platform/editor/common/editor'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { Themable } from 'vs/workbench/common/theme'; import { ipcRenderer as ipc, webFrame } from 'electron'; @@ -38,7 +35,6 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { fillInActionBarActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; @@ -46,6 +42,8 @@ import { AccessibilitySupport, isRootUser, isWindows, isMacintosh } from 'vs/bas import product from 'vs/platform/node/product'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; const TextInputActions: IAction[] = [ new Action('undo', nls.localize('undo', "Undo"), null, true, () => document.execCommand('undo') && TPromise.as(true)), @@ -60,8 +58,6 @@ const TextInputActions: IAction[] = [ export class ElectronWindow extends Themable { - private static readonly AUTO_SAVE_SETTING = 'files.autoSave'; - private touchBarMenu: IMenu; private touchBarUpdater: RunOnceScheduler; private touchBarDisposables: IDisposable[]; @@ -70,10 +66,9 @@ export class ElectronWindow extends Themable { private previousConfiguredZoomLevel: number; private addFoldersScheduler: RunOnceScheduler; - private pendingFoldersToAdd: IAddFoldersRequest[]; + private pendingFoldersToAdd: URI[]; constructor( - shellContainer: HTMLElement, @IEditorService private editorService: EditorServiceImpl, @IWindowsService private windowsService: IWindowsService, @IWindowService private windowService: IWindowService, @@ -97,8 +92,7 @@ export class ElectronWindow extends Themable { this.touchBarDisposables = []; this.pendingFoldersToAdd = []; - this.addFoldersScheduler = new RunOnceScheduler(() => this.doAddFolders(), 100); - this.toUnbind.push(this.addFoldersScheduler); + this.addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100)); this.registerListeners(); this.create(); @@ -107,7 +101,7 @@ export class ElectronWindow extends Themable { private registerListeners(): void { // React to editor input changes - this.toUnbind.push(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); // prevent opening a real URL inside the shell [DOM.EventType.DRAG_OVER, DOM.EventType.DROP].forEach(event => { @@ -134,7 +128,7 @@ export class ElectronWindow extends Themable { args.push({ from: request.from }); // TODO@telemetry this is a bit weird to send this to every action? } - this.commandService.executeCommand(request.id, ...args).done(_ => { + this.commandService.executeCommand(request.id, ...args).then(_ => { /* __GDPR__ "commandExecuted" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -157,11 +151,11 @@ export class ElectronWindow extends Themable { } // Resolve keys using the keybinding service and send back to browser process - this.resolveKeybindings(actionIds).done(keybindings => { + this.resolveKeybindings(actionIds).then(keybindings => { if (keybindings.length) { ipc.send('vscode:keybindingsResolved', JSON.stringify(keybindings)); } - }, () => errors.onUnexpectedError); + }); }); ipc.on('vscode:reportError', (event: any, error: string) => { @@ -183,11 +177,6 @@ export class ElectronWindow extends Themable { this.notificationService.info(message); }); - // Support toggling auto save - ipc.on('vscode.toggleAutoSave', () => { - this.toggleAutoSave(); - }); - // Fullscreen Events ipc.on('vscode:enterFullScreen', () => { this.lifecycleService.when(LifecyclePhase.Running).then(() => { @@ -215,7 +204,7 @@ export class ElectronWindow extends Themable { const windowConfig = this.configurationService.getValue('window'); if (windowConfig && windowConfig.autoDetectHighContrast) { this.lifecycleService.when(LifecyclePhase.Running).then(() => { - this.themeService.setColorTheme(VS_DARK_THEME, null); + this.themeService.restoreColorTheme(); }); } }); @@ -232,7 +221,7 @@ export class ElectronWindow extends Themable { // Zoom level changes this.updateWindowZoomLevel(); - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('window.zoomLevel')) { this.updateWindowZoomLevel(); } @@ -246,12 +235,12 @@ export class ElectronWindow extends Themable { if (e.target instanceof HTMLElement) { const target = e.target; if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) { - e.preventDefault(); - e.stopPropagation(); + DOM.EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ getAnchor: () => e, - getActions: () => TPromise.as(TextInputActions) + getActions: () => TPromise.as(TextInputActions), + onHide: () => target.focus() // fixes https://github.com/Microsoft/vscode/issues/52948 }); } } @@ -394,19 +383,16 @@ export class ElectronWindow extends Themable { if (!binding) { return null; } - // first try to resolve a native accelerator const electronAccelerator = binding.getElectronAccelerator(); if (electronAccelerator) { return { id, label: electronAccelerator, isNative: true }; } - // we need this fallback to support keybindings that cannot show in electron menus (e.g. chords) const acceleratorLabel = binding.getLabel(); if (acceleratorLabel) { return { id, label: acceleratorLabel, isNative: false }; } - return null; })); }); @@ -415,7 +401,7 @@ export class ElectronWindow extends Themable { private onAddFoldersRequest(request: IAddFoldersRequest): void { // Buffer all pending requests - this.pendingFoldersToAdd.push(request); + this.pendingFoldersToAdd.push(...request.foldersToAdd.map(f => URI.revive(f))); // Delay the adding of folders a bit to buffer in case more requests are coming if (!this.addFoldersScheduler.isScheduled()) { @@ -426,13 +412,13 @@ export class ElectronWindow extends Themable { private doAddFolders(): void { const foldersToAdd: IWorkspaceFolderCreationData[] = []; - this.pendingFoldersToAdd.forEach(request => { - foldersToAdd.push(...request.foldersToAdd.map(folderToAdd => ({ uri: URI.file(folderToAdd.filePath) }))); + this.pendingFoldersToAdd.forEach(folder => { + foldersToAdd.push(({ uri: folder })); }); this.pendingFoldersToAdd = []; - this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError); + this.workspaceEditingService.addFolders(foldersToAdd); } private onOpenFiles(request: IOpenFileRequest): void { @@ -459,12 +445,12 @@ export class ElectronWindow extends Themable { // In wait mode, listen to changes to the editors and wait until the files // are closed that the user wants to wait for. When this happens we delete // the wait marker file to signal to the outside that editing is done. - const resourcesToWaitFor = request.filesToWait.paths.map(p => URI.file(p.filePath)); + const resourcesToWaitFor = request.filesToWait.paths.map(p => URI.revive(p.fileUri)); const waitMarkerFile = URI.file(request.filesToWait.waitMarkerFilePath); const unbind = this.editorService.onDidCloseEditor(() => { if (resourcesToWaitFor.every(resource => !this.editorService.isOpen({ resource }))) { unbind.dispose(); - this.fileService.del(waitMarkerFile).done(null, errors.onUnexpectedError); + this.fileService.del(waitMarkerFile); } }); } @@ -488,9 +474,9 @@ export class ElectronWindow extends Themable { }); } - private toInputs(paths: IPath[], isNew: boolean): IResourceEditor[] { + private toInputs(paths: IPathData[], isNew: boolean): IResourceEditor[] { return paths.map(p => { - const resource = URI.file(p.filePath); + const resource = URI.revive(p.fileUri); let input: IResourceInput | IUntitledResourceInput; if (isNew) { input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput; @@ -509,24 +495,7 @@ export class ElectronWindow extends Themable { }); } - private toggleAutoSave(): void { - const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING); - let userAutoSaveConfig = setting.user; - if (types.isUndefinedOrNull(userAutoSaveConfig)) { - userAutoSaveConfig = setting.default; // use default if setting not defined - } - - let newAutoSaveValue: string; - if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) { - newAutoSaveValue = AutoSaveConfiguration.OFF; - } else { - newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY; - } - - this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER); - } - - public dispose(): void { + dispose(): void { this.touchBarDisposables = dispose(this.touchBarDisposables); super.dispose(); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 040fb9c88f3..71b75413be6 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -12,18 +12,17 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as DOM from 'vs/base/browser/dom'; -import { Builder, $ } from 'vs/base/browser/builder'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as browser from 'vs/base/browser/browser'; import * as perf from 'vs/base/common/performance'; import * as errors from 'vs/base/common/errors'; -import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService'; +import { BackupFileService, InMemoryBackupFileService } from 'vs/workbench/services/backup/node/backupFileService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Registry } from 'vs/platform/registry/common/platform'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IEditorInputFactoryRegistry, Extensions as EditorExtensions, TextCompareEditorVisibleContext, TEXT_DIFF_EDITOR_ID, EditorsVisibleContext, InEditorZenModeContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, IUntitledResourceInput, IResourceDiffInput, SplitEditorsVertically } from 'vs/workbench/common/editor'; +import { IEditorInputFactoryRegistry, Extensions as EditorExtensions, TextCompareEditorVisibleContext, TEXT_DIFF_EDITOR_ID, EditorsVisibleContext, InEditorZenModeContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, IUntitledResourceInput, IResourceDiffInput, SplitEditorsVertically, TextCompareEditorActiveContext, ActiveEditorContext } from 'vs/workbench/common/editor'; import { HistoryService } from 'vs/workbench/services/history/electron-browser/history'; import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; @@ -38,10 +37,11 @@ import { QuickOpenController } from 'vs/workbench/browser/parts/quickopen/quickO 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 } from 'vs/workbench/services/part/common/partService'; +import { Position, Parts, IPartService, ILayoutOptions, IDimension, PositionToString } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; +import { ContextMenuService as NativeContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; +import { ContextMenuService as HTMLContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; import { WorkbenchKeybindingService } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WorkspaceService, DefaultConfigurationExportHelper } from 'vs/workbench/services/configuration/node/configurationService'; @@ -70,30 +70,30 @@ import { TextFileService } from 'vs/workbench/services/textfile/electron-browser import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ISCMService } from 'vs/workbench/services/scm/common/scm'; import { SCMService } from 'vs/workbench/services/scm/common/scmService'; -import { IProgressService2 } from 'vs/platform/progress/common/progress'; +import { IProgressService2 } from 'vs/workbench/services/progress/common/progress'; import { ProgressService2 } from 'vs/workbench/services/progress/browser/progressService2'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecycleService } from 'vs/platform/lifecycle/electron-browser/lifecycleService'; -import { IWindowService, IWindowConfiguration as IWindowSettings, IWindowConfiguration, IPath } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowConfiguration as IWindowSettings, IWindowConfiguration, IPath, MenuBarVisibility } from 'vs/platform/windows/common/windows'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IMenuService, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { MenuService } from 'vs/workbench/services/actions/common/menuService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +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 } from 'vs/workbench/electron-browser/actions'; +import { OpenRecentAction, ToggleDevToolsAction, ReloadWindowAction, ShowPreviousWindowTab, MoveWindowTabToNewWindow, MergeAllWindowTabs, ShowNextWindowTab, ToggleWindowTabsBar, ReloadWindowWithExtensionsDisabledAction, NewWindowTab } from 'vs/workbench/electron-browser/actions'; 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'; import { FileDecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { ActivityService } from 'vs/workbench/services/activity/browser/activityService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; -import { InputFocusedContext } from 'vs/platform/workbench/common/contextkeys'; +import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, FileDialogContext } from 'vs/platform/workbench/common/contextkeys'; import { IViewsService } from 'vs/workbench/common/views'; import { ViewsService } from 'vs/workbench/browser/parts/views/views'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -103,14 +103,20 @@ import { NotificationsAlerts } from 'vs/workbench/browser/parts/notifications/no import { NotificationsStatus } from 'vs/workbench/browser/parts/notifications/notificationsStatus'; import { registerNotificationCommands } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; -import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IPCClient } from 'vs/base/parts/ipc/node/ipc'; import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { PreferencesService } from 'vs/workbench/services/preferences/browser/preferencesService'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService, GroupDirection, preferredSideBySideGroupDirection, GroupOrientation } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroupsService, GroupDirection, preferredSideBySideGroupDirection } from 'vs/workbench/services/group/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IExtensionUrlHandler, ExtensionUrlHandler } from 'vs/platform/url/electron-browser/inactiveExtensionUrlHandler'; +import { IExtensionUrlHandler, ExtensionUrlHandler } from 'vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { LabelService, ILabelService } from 'vs/platform/label/common/label'; interface WorkbenchParams { configuration: IWindowConfiguration; @@ -167,6 +173,7 @@ interface IZenMode { export class Workbench extends Disposable implements IPartService { private static readonly sidebarHiddenStorageKey = 'workbench.sidebar.hidden'; + private static readonly menubarVisibilityConfigurationKey = 'window.menuBarVisibility'; private static readonly sidebarRestoreStorageKey = 'workbench.sidebar.restore'; private static readonly panelHiddenStorageKey = 'workbench.panel.hidden'; private static readonly zenModeActiveStorageKey = 'workbench.zenmode.active'; @@ -182,8 +189,7 @@ export class Workbench extends Disposable implements IPartService { _serviceBrand: any; private workbenchParams: WorkbenchParams; - private workbenchContainer: Builder; - private workbench: Builder; + private workbench: HTMLElement; private workbenchStarted: boolean; private workbenchCreated: boolean; private workbenchShutdown: boolean; @@ -191,6 +197,7 @@ export class Workbench extends Disposable implements IPartService { private editorService: EditorService; private editorGroupService: IEditorGroupsService; private viewletService: IViewletService; + private contextViewService: ContextViewService; private contextKeyService: IContextKeyService; private keybindingService: IKeybindingService; private backupFileService: IBackupFileService; @@ -212,21 +219,21 @@ export class Workbench extends Disposable implements IPartService { private sideBarHidden: boolean; private statusBarHidden: boolean; private activityBarHidden: boolean; + private menubarToggled: boolean; private sideBarPosition: Position; private panelPosition: Position; private panelHidden: boolean; + private menubarVisibility: MenuBarVisibility; private zenMode: IZenMode; - private centeredEditorLayoutActive: boolean; private fontAliasing: FontAliasingOption; private hasInitialFilesToOpen: boolean; private inZenMode: IContextKey; private sideBarVisibleContext: IContextKey; - private closeEmptyWindowScheduler: RunOnceScheduler = new RunOnceScheduler(() => this.onAllEditorsClosed(), 50); + private closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50)); constructor( - private parent: HTMLElement, private container: HTMLElement, private configuration: IWindowConfiguration, serviceCollection: ServiceCollection, @@ -236,9 +243,11 @@ export class Workbench extends Disposable implements IPartService { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IStorageService private storageService: IStorageService, @IConfigurationService private configurationService: WorkspaceService, + @IWorkbenchThemeService private themeService: WorkbenchThemeService, @IEnvironmentService private environmentService: IEnvironmentService, @IWindowService private windowService: IWindowService, - @INotificationService private notificationService: NotificationService + @INotificationService private notificationService: NotificationService, + @ITelemetryService private telemetryService: TelemetryService ) { super(); @@ -287,11 +296,13 @@ export class Workbench extends Disposable implements IPartService { } private createWorkbench(): void { - this.workbenchContainer = $('.monaco-workbench-container'); - this.workbench = $().div({ - 'class': `monaco-workbench ${isWindows ? 'windows' : isLinux ? 'linux' : 'mac'}`, - id: Identifiers.WORKBENCH_CONTAINER - }).appendTo(this.workbenchContainer); + this.workbench = document.createElement('div'); + this.workbench.id = Identifiers.WORKBENCH_CONTAINER; + DOM.addClasses(this.workbench, 'monaco-workbench', isWindows ? 'windows' : isLinux ? 'linux' : 'mac'); + + this._register(DOM.addDisposableListener(this.workbench, DOM.EventType.SCROLL, () => { + this.workbench.scrollTop = 0; // Prevent workbench from scrolling #55456 + })); } private createGlobalActions(): void { @@ -307,6 +318,7 @@ export class Workbench extends Disposable implements IPartService { // Actions for macOS native tabs management (only when enabled) const windowConfig = this.configurationService.getValue(); if (windowConfig && windowConfig.window && windowConfig.window.nativeTabs) { + registry.registerWorkbenchAction(new SyncActionDescriptor(NewWindowTab, NewWindowTab.ID, NewWindowTab.LABEL), 'New Window Tab'); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousWindowTab, ShowPreviousWindowTab.ID, ShowPreviousWindowTab.LABEL), 'Show Previous Window Tab'); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextWindowTab, ShowNextWindowTab.ID, ShowNextWindowTab.LABEL), 'Show Next Window Tab'); registry.registerWorkbenchAction(new SyncActionDescriptor(MoveWindowTabToNewWindow, MoveWindowTabToNewWindow.ID, MoveWindowTabToNewWindow.LABEL), 'Move Window Tab to New Window'); @@ -324,6 +336,10 @@ export class Workbench extends Disposable implements IPartService { // Clipboard serviceCollection.set(IClipboardService, new ClipboardService()); + // Uri Display + const labelService = new LabelService(this.environmentService, this.contextService); + serviceCollection.set(ILabelService, labelService); + // Status bar this.statusbarPart = this.instantiationService.createInstance(StatusbarPart, Identifiers.STATUSBAR_PART); this._register(toDisposable(() => this.statusbarPart.shutdown())); @@ -342,8 +358,16 @@ export class Workbench extends Disposable implements IPartService { // List serviceCollection.set(IListService, this.instantiationService.createInstance(ListService)); - // Context Menu - serviceCollection.set(IContextMenuService, new SyncDescriptor(ContextMenuService)); + // Context view service + this.contextViewService = this.instantiationService.createInstance(ContextViewService, this.workbench); + serviceCollection.set(IContextViewService, this.contextViewService); + + // Use themable context menus when custom titlebar is enabled to match custom menubar + if (!isMacintosh && this.getCustomTitleBarStyle() === 'custom') { + serviceCollection.set(IContextMenuService, new SyncDescriptor(HTMLContextMenuService, null, this.telemetryService, this.notificationService, this.contextViewService)); + } else { + serviceCollection.set(IContextMenuService, new SyncDescriptor(NativeContextMenuService)); + } // Menus/Actions serviceCollection.set(IMenuService, new SyncDescriptor(MenuService)); @@ -375,6 +399,7 @@ export class Workbench extends Disposable implements IPartService { this.fileService = this.instantiationService.createInstance(RemoteFileService); serviceCollection.set(IFileService, this.fileService); this.configurationService.acquireFileService(this.fileService); + this.themeService.acquireFileService(this.fileService); // Editor and Group services const restorePreviousEditorState = !this.hasInitialFilesToOpen; @@ -389,11 +414,16 @@ export class Workbench extends Disposable implements IPartService { this.titlebarPart = this.instantiationService.createInstance(TitlebarPart, Identifiers.TITLEBAR_PART); this._register(toDisposable(() => this.titlebarPart.shutdown())); serviceCollection.set(ITitleService, this.titlebarPart); + // History serviceCollection.set(IHistoryService, new SyncDescriptor(HistoryService)); // Backup File Service - this.backupFileService = this.instantiationService.createInstance(BackupFileService, this.workbenchParams.configuration.backupPath); + if (this.workbenchParams.configuration.backupPath) { + this.backupFileService = this.instantiationService.createInstance(BackupFileService, this.workbenchParams.configuration.backupPath); + } else { + this.backupFileService = new InMemoryBackupFileService(); + } serviceCollection.set(IBackupFileService, this.backupFileService); // Text File Service @@ -463,7 +493,7 @@ export class Workbench extends Disposable implements IPartService { // Listen to editor closing (if we run with --wait) const filesToWait = this.workbenchParams.configuration.filesToWait; if (filesToWait) { - const resourcesToWaitFor = filesToWait.paths.map(p => URI.file(p.filePath)); + const resourcesToWaitFor = filesToWait.paths.map(p => p.fileUri); const waitMarkerFile = URI.file(filesToWait.waitMarkerFilePath); const listenerDispose = this.editorService.onDidCloseEditor(() => this.onEditorClosed(listenerDispose, resourcesToWaitFor, waitMarkerFile)); @@ -485,9 +515,10 @@ export class Workbench extends Disposable implements IPartService { // Apply as CSS class const isFullscreen = browser.isFullscreen(); if (isFullscreen) { - this.workbench.addClass('fullscreen'); + DOM.addClass(this.workbench, 'fullscreen'); } else { - this.workbench.removeClass('fullscreen'); + DOM.removeClass(this.workbench, 'fullscreen'); + if (this.zenMode.transitionedToFullScreen && this.zenMode.active) { this.toggleZenMode(); } @@ -501,6 +532,20 @@ export class Workbench extends Disposable implements IPartService { } } + private onMenubarToggled(visible: boolean) { + if (visible !== this.menubarToggled) { + this.menubarToggled = visible; + + if (this.menubarVisibility === 'toggle' || (browser.isFullscreen() && this.menubarVisibility === 'default')) { + if (browser.isFullscreen() && this.menubarVisibility === 'default') { + this._onTitleBarVisibilityChange.fire(); + } + + this.layout(); + } + } + } + private onEditorClosed(listenerDispose: IDisposable, resourcesToWaitFor: URI[], waitMarkerFile: URI): void { // In wait mode, listen to changes to the editors and wait until the files @@ -508,7 +553,7 @@ export class Workbench extends Disposable implements IPartService { // the wait marker file to signal to the outside that editing is done. if (resourcesToWaitFor.every(resource => !this.editorService.isOpen({ resource }))) { listenerDispose.dispose(); - this.fileService.del(waitMarkerFile).done(null, errors.onUnexpectedError); + this.fileService.del(waitMarkerFile); } } @@ -558,6 +603,9 @@ export class Workbench extends Disposable implements IPartService { this.setActivityBarHidden(newActivityBarHiddenValue, skipLayout); } } + + const newMenubarVisibility = this.configurationService.getValue(Workbench.menubarVisibilityConfigurationKey); + this.setMenubarVisibility(newMenubarVisibility, skipLayout); } //#endregion @@ -565,17 +613,26 @@ export class Workbench extends Disposable implements IPartService { private handleContextKeys(): void { this.inZenMode = InEditorZenModeContext.bindTo(this.contextKeyService); + IsMacContext.bindTo(this.contextKeyService); + IsLinuxContext.bindTo(this.contextKeyService); + IsWindowsContext.bindTo(this.contextKeyService); + FileDialogContext.bindTo(this.contextKeyService); + const sidebarVisibleContextRaw = new RawContextKey('sidebarVisible', false); this.sideBarVisibleContext = sidebarVisibleContextRaw.bindTo(this.contextKeyService); + const activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); const editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); const textCompareEditorVisible = TextCompareEditorVisibleContext.bindTo(this.contextKeyService); + const textCompareEditorActive = TextCompareEditorActiveContext.bindTo(this.contextKeyService); const activeEditorGroupEmpty = ActiveEditorGroupEmptyContext.bindTo(this.contextKeyService); const multipleEditorGroups = MultipleEditorGroupsContext.bindTo(this.contextKeyService); const updateEditorContextKeys = () => { + const activeControl = this.editorService.activeControl; const visibleEditors = this.editorService.visibleControls; + textCompareEditorActive.set(activeControl && activeControl.getId() === TEXT_DIFF_EDITOR_ID); textCompareEditorVisible.set(visibleEditors.some(control => control.getId() === TEXT_DIFF_EDITOR_ID)); if (visibleEditors.length > 0) { @@ -595,6 +652,12 @@ export class Workbench extends Disposable implements IPartService { } else { multipleEditorGroups.reset(); } + + if (activeControl) { + activeEditorContext.set(activeControl.getId()); + } else { + activeEditorContext.reset(); + } }; this.editorPart.whenRestored.then(() => updateEditorContextKeys()); @@ -678,17 +741,20 @@ export class Workbench extends Disposable implements IPartService { const panelRegistry = Registry.as(PanelExtensions.Panels); const panelId = this.storageService.get(PanelPart.activePanelSettingsKey, StorageScope.WORKSPACE, panelRegistry.getDefaultPanelId()); if (!this.panelHidden && !!panelId) { - restorePromises.push(this.panelPart.openPanel(panelId, false)); + perf.mark('willRestorePanel'); + const isPanelToRestoreEnabled = !!this.panelPart.getPanels().filter(p => p.id === panelId).length; + const panelIdToRestore = isPanelToRestoreEnabled ? panelId : panelRegistry.getDefaultPanelId(); + restorePromises.push(this.panelPart.openPanel(panelIdToRestore, false).then(() => perf.mark('didRestorePanel'))); } // Restore Zen Mode if active if (this.storageService.getBoolean(Workbench.zenModeActiveStorageKey, StorageScope.WORKSPACE, false)) { - this.toggleZenMode(true); + this.toggleZenMode(true, true); } // Restore Forced Editor Center Mode if (this.storageService.getBoolean(Workbench.centeredEditorLayoutActiveStorageKey, StorageScope.WORKSPACE, false)) { - this.centeredEditorLayoutActive = true; + this.centerEditorLayout(true); } const onRestored = (error?: Error): IWorkbenchStartedInfo => { @@ -734,7 +800,8 @@ export class Workbench extends Disposable implements IPartService { return TPromise.as([{ leftResource: filesToDiff[0].resource, rightResource: filesToDiff[1].resource, - options: { pinned: true } + options: { pinned: true }, + forceFile: true }]); } @@ -770,12 +837,12 @@ export class Workbench extends Disposable implements IPartService { } return paths.map(p => { - const resource = URI.file(p.filePath); + const resource = p.fileUri; let input: IResourceInput | IUntitledResourceInput; if (isNew) { input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput; } else { - input = { resource, options: { pinned: true } } as IResourceInput; + input = { resource, options: { pinned: true }, forceFile: true } as IResourceInput; } if (!isNew && p.lineNumber) { @@ -822,6 +889,10 @@ export class Workbench extends Disposable implements IPartService { // Panel position this.setPanelPositionFromStorageOrConfig(); + // Menubar visibility + const menuBarVisibility = this.configurationService.getValue(Workbench.menubarVisibilityConfigurationKey); + this.setMenubarVisibility(menuBarVisibility, true); + // Statusbar visibility const statusBarVisible = this.configurationService.getValue(Workbench.statusbarVisibleConfigurationKey); this.statusBarHidden = !statusBarVisible; @@ -842,9 +913,6 @@ export class Workbench extends Disposable implements IPartService { wasPanelVisible: false, transitionDisposeables: [] }; - - // Centered Editor Layout - this.centeredEditorLayoutActive = false; } private setPanelPositionFromStorageOrConfig() { @@ -854,12 +922,8 @@ export class Workbench extends Disposable implements IPartService { } private getCustomTitleBarStyle(): 'custom' { - if (!isMacintosh) { - return null; // custom title bar is only supported on Mac currently - } - const isDev = !this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment; - if (isDev) { + if (isMacintosh && isDev) { return null; // not enabled when developing due to https://github.com/electron/electron/issues/3647 } @@ -884,9 +948,9 @@ export class Workbench extends Disposable implements IPartService { // Adjust CSS if (hidden) { - this.workbench.addClass('nostatusbar'); + DOM.addClass(this.workbench, 'nostatusbar'); } else { - this.workbench.removeClass('nostatusbar'); + DOM.removeClass(this.workbench, 'nostatusbar'); } // Layout @@ -911,7 +975,7 @@ export class Workbench extends Disposable implements IPartService { this.workbenchLayout = this.instantiationService.createInstance( WorkbenchLayout, this.container, - this.workbench.getHTMLElement(), + this.workbench, { titlebar: this.titlebarPart, activitybar: this.activitybarPart, @@ -931,27 +995,23 @@ export class Workbench extends Disposable implements IPartService { // Apply sidebar state as CSS class if (this.sideBarHidden) { - this.workbench.addClass('nosidebar'); + DOM.addClass(this.workbench, 'nosidebar'); } + if (this.panelHidden) { - this.workbench.addClass('nopanel'); + DOM.addClass(this.workbench, 'nopanel'); } + if (this.statusBarHidden) { - this.workbench.addClass('nostatusbar'); + DOM.addClass(this.workbench, 'nostatusbar'); } // Apply font aliasing this.setFontAliasing(this.fontAliasing); - // Apply title style if shown - const titleStyle = this.getCustomTitleBarStyle(); - if (titleStyle) { - DOM.addClass(this.parent, `titlebar-style-${titleStyle}`); - } - // Apply fullscreen state if (browser.isFullscreen()) { - this.workbench.addClass('fullscreen'); + DOM.addClass(this.workbench, 'fullscreen'); } // Create Parts @@ -965,81 +1025,70 @@ export class Workbench extends Disposable implements IPartService { // Notification Handlers this.createNotificationsHandlers(); + + // Menubar visibility changes + if ((isWindows || isLinux) && this.getCustomTitleBarStyle() === 'custom') { + this.titlebarPart.onMenubarVisibilityChange()(e => this.onMenubarToggled(e)); + } + // Add Workbench to DOM - this.workbenchContainer.build(this.container); + this.container.appendChild(this.workbench); } private createTitlebarPart(): void { - const titlebarContainer = $(this.workbench).div({ - 'class': ['part', 'titlebar'], - id: Identifiers.TITLEBAR_PART, - role: 'contentinfo' - }); + const titlebarContainer = this.createPart(Identifiers.TITLEBAR_PART, ['part', 'titlebar'], 'contentinfo'); - this.titlebarPart.create(titlebarContainer.getHTMLElement()); + this.titlebarPart.create(titlebarContainer); } private createActivityBarPart(): void { - const activitybarPartContainer = $(this.workbench) - .div({ - 'class': ['part', 'activitybar', this.sideBarPosition === Position.LEFT ? 'left' : 'right'], - id: Identifiers.ACTIVITYBAR_PART, - role: 'navigation' - }); + const activitybarPartContainer = this.createPart(Identifiers.ACTIVITYBAR_PART, ['part', 'activitybar', this.sideBarPosition === Position.LEFT ? 'left' : 'right'], 'navigation'); - this.activitybarPart.create(activitybarPartContainer.getHTMLElement()); + this.activitybarPart.create(activitybarPartContainer); } private createSidebarPart(): void { - const sidebarPartContainer = $(this.workbench) - .div({ - 'class': ['part', 'sidebar', this.sideBarPosition === Position.LEFT ? 'left' : 'right'], - id: Identifiers.SIDEBAR_PART, - role: 'complementary' - }); + const sidebarPartContainer = this.createPart(Identifiers.SIDEBAR_PART, ['part', 'sidebar', this.sideBarPosition === Position.LEFT ? 'left' : 'right'], 'complementary'); - this.sidebarPart.create(sidebarPartContainer.getHTMLElement()); + this.sidebarPart.create(sidebarPartContainer); } private createPanelPart(): void { - const panelPartContainer = $(this.workbench) - .div({ - 'class': ['part', 'panel', this.panelPosition === Position.BOTTOM ? 'bottom' : 'right'], - id: Identifiers.PANEL_PART, - role: 'complementary' - }); + const panelPartContainer = this.createPart(Identifiers.PANEL_PART, ['part', 'panel', this.panelPosition === Position.BOTTOM ? 'bottom' : 'right'], 'complementary'); - this.panelPart.create(panelPartContainer.getHTMLElement()); + this.panelPart.create(panelPartContainer); } private createEditorPart(): void { - const editorContainer = $(this.workbench) - .div({ - 'class': ['part', 'editor'], - id: Identifiers.EDITOR_PART, - role: 'main' - }); + const editorContainer = this.createPart(Identifiers.EDITOR_PART, ['part', 'editor'], 'main'); - this.editorPart.create(editorContainer.getHTMLElement()); + this.editorPart.create(editorContainer); } private createStatusbarPart(): void { - const statusbarContainer = $(this.workbench).div({ - 'class': ['part', 'statusbar'], - id: Identifiers.STATUSBAR_PART, - role: 'contentinfo' - }); + const statusbarContainer = this.createPart(Identifiers.STATUSBAR_PART, ['part', 'statusbar'], 'contentinfo'); - this.statusbarPart.create(statusbarContainer.getHTMLElement()); + this.statusbarPart.create(statusbarContainer); + } + + private createPart(id: string, classes: string[], role: string): HTMLElement { + const part = document.createElement('div'); + classes.forEach(clazz => DOM.addClass(part, clazz)); + part.id = id; + part.setAttribute('role', role); + + this.workbench.appendChild(part); + + return part; } private createNotificationsHandlers(): void { // Notifications Center - this.notificationsCenter = this._register(this.instantiationService.createInstance(NotificationsCenter, this.workbench.getHTMLElement(), this.notificationService.model)); + this.notificationsCenter = this._register(this.instantiationService.createInstance(NotificationsCenter, this.workbench, this.notificationService.model)); // Notifications Toasts - this.notificationsToasts = this._register(this.instantiationService.createInstance(NotificationsToasts, this.workbench.getHTMLElement(), this.notificationService.model)); + this.notificationsToasts = this._register(this.instantiationService.createInstance(NotificationsToasts, this.workbench, this.notificationService.model)); // Notifications Alerts this._register(this.instantiationService.createInstance(NotificationsAlerts, this.notificationService.model)); @@ -1090,13 +1139,13 @@ export class Workbench extends Disposable implements IPartService { //#region IPartService - private _onTitleBarVisibilityChange: Emitter = new Emitter(); + private _onTitleBarVisibilityChange: Emitter = this._register(new Emitter()); get onTitleBarVisibilityChange(): Event { return this._onTitleBarVisibilityChange.event; } get onEditorLayout(): Event { return this.editorPart.onDidLayout; } isCreated(): boolean { - return this.workbenchCreated && this.workbenchStarted; + return !!(this.workbenchCreated && this.workbenchStarted); } hasFocus(part: Parts): boolean { @@ -1138,7 +1187,19 @@ export class Workbench extends Disposable implements IPartService { isVisible(part: Parts): boolean { switch (part) { case Parts.TITLEBAR_PART: - return this.getCustomTitleBarStyle() && !browser.isFullscreen(); + if (this.getCustomTitleBarStyle() !== 'custom') { + return false; + } else if (!browser.isFullscreen()) { + return true; + } else if (isMacintosh) { + return false; + } else if (this.menubarVisibility === 'visible') { + return true; + } else if (this.menubarVisibility === 'toggle' || this.menubarVisibility === 'default') { + return this.menubarToggled; + } + + return false; case Parts.SIDEBAR_PART: return !this.sideBarHidden; case Parts.PANEL_PART: @@ -1155,17 +1216,20 @@ export class Workbench extends Disposable implements IPartService { getTitleBarOffset(): number { let offset = 0; if (this.isVisible(Parts.TITLEBAR_PART)) { - offset = 22 / browser.getZoomFactor(); // adjust the position based on title bar size and zoom factor + offset = this.workbenchLayout.partLayoutInfo.titlebar.height; + if (isMacintosh || this.menubarVisibility === 'hidden') { + offset /= browser.getZoomFactor(); + } } return offset; } - getWorkbenchElementId(): string { - return Identifiers.WORKBENCH_CONTAINER; + getWorkbenchElement(): HTMLElement { + return this.workbench; } - toggleZenMode(skipLayout?: boolean): void { + toggleZenMode(skipLayout?: boolean, restoring = false): void { this.zenMode.active = !this.zenMode.active; this.zenMode.transitionDisposeables = dispose(this.zenMode.transitionDisposeables); @@ -1178,13 +1242,13 @@ export class Workbench extends Disposable implements IPartService { const config = this.configurationService.getValue('zenMode'); toggleFullScreen = !browser.isFullscreen() && config.fullScreen; - this.zenMode.transitionedToFullScreen = toggleFullScreen; + this.zenMode.transitionedToFullScreen = restoring ? config.fullScreen : toggleFullScreen; this.zenMode.transitionedToCenteredEditorLayout = !this.isEditorLayoutCentered() && config.centerLayout; this.zenMode.wasSideBarVisible = this.isVisible(Parts.SIDEBAR_PART); this.zenMode.wasPanelVisible = this.isVisible(Parts.PANEL_PART); - this.setPanelHidden(true, true).done(void 0, errors.onUnexpectedError); - this.setSideBarHidden(true, true).done(void 0, errors.onUnexpectedError); + this.setPanelHidden(true, true); + this.setSideBarHidden(true, true); if (config.hideActivityBar) { this.setActivityBarHidden(true, true); @@ -1206,11 +1270,11 @@ export class Workbench extends Disposable implements IPartService { // Zen Mode Inactive else { if (this.zenMode.wasPanelVisible) { - this.setPanelHidden(false, true).done(void 0, errors.onUnexpectedError); + this.setPanelHidden(false, true); } if (this.zenMode.wasSideBarVisible) { - this.setSideBarHidden(false, true).done(void 0, errors.onUnexpectedError); + this.setSideBarHidden(false, true); } if (this.zenMode.transitionedToCenteredEditorLayout) { @@ -1232,50 +1296,27 @@ export class Workbench extends Disposable implements IPartService { } if (toggleFullScreen) { - this.windowService.toggleFullScreen().done(void 0, errors.onUnexpectedError); + this.windowService.toggleFullScreen(); } } layout(options?: ILayoutOptions): void { + this.contextViewService.layout(); + if (this.workbenchStarted && !this.workbenchShutdown) { this.workbenchLayout.layout(options); } } isEditorLayoutCentered(): boolean { - return this.centeredEditorLayoutActive; + return this.editorPart.isLayoutCentered(); } - // TODO@ben support centered editor layout using empty groups or not? functionality missing: - // - resize sashes left and right in sync - // - IEditorInput.supportsCenteredEditorLayout() no longer supported - // - should we just allow to enter layout even if groups > 1? what does it then mean to be - // actively in centered editor layout though? centerEditorLayout(active: boolean, skipLayout?: boolean): void { - this.centeredEditorLayoutActive = active; - this.storageService.store(Workbench.centeredEditorLayoutActiveStorageKey, this.centeredEditorLayoutActive, StorageScope.WORKSPACE); + this.storageService.store(Workbench.centeredEditorLayoutActiveStorageKey, active, StorageScope.WORKSPACE); // Enter Centered Editor Layout - if (active) { - if (this.editorGroupService.count === 1) { - const activeGroup = this.editorGroupService.activeGroup; - this.editorGroupService.addGroup(activeGroup, GroupDirection.LEFT); - this.editorGroupService.addGroup(activeGroup, GroupDirection.RIGHT); - - this.editorGroupService.applyLayout({ groups: [{ size: 0.2 }, { size: 0.6 }, { size: 0.2 }], orientation: GroupOrientation.HORIZONTAL }); - } - } - - // Leave Centered Editor Layout - else { - if (this.editorGroupService.count === 3) { - this.editorGroupService.groups.forEach(group => { - if (group.count === 0) { - this.editorGroupService.removeGroup(group); - } - }); - } - } + this.editorPart.centerLayout(active); if (!skipLayout) { this.layout(); @@ -1309,9 +1350,9 @@ export class Workbench extends Disposable implements IPartService { // Adjust CSS if (hidden) { - this.workbench.addClass('nosidebar'); + DOM.addClass(this.workbench, 'nosidebar'); } else { - this.workbench.removeClass('nosidebar'); + DOM.removeClass(this.workbench, 'nosidebar'); } // If sidebar becomes hidden, also hide the current active Viewlet if any @@ -1360,9 +1401,9 @@ export class Workbench extends Disposable implements IPartService { // Adjust CSS if (hidden) { - this.workbench.addClass('nopanel'); + DOM.addClass(this.workbench, 'nopanel'); } else { - this.workbench.removeClass('nopanel'); + DOM.removeClass(this.workbench, 'nopanel'); } // If panel part becomes hidden, also hide the current active panel if any @@ -1411,7 +1452,7 @@ export class Workbench extends Disposable implements IPartService { setSideBarPosition(position: Position): void { if (this.sideBarHidden) { - this.setSideBarHidden(false, true /* Skip Layout */).done(void 0, errors.onUnexpectedError); + this.setSideBarHidden(false, true /* Skip Layout */); } const newPositionValue = (position === Position.LEFT) ? 'left' : 'right'; @@ -1432,6 +1473,20 @@ export class Workbench extends Disposable implements IPartService { this.workbenchLayout.layout(); } + setMenubarVisibility(visibility: MenuBarVisibility, skipLayout: boolean): void { + if (this.menubarVisibility !== visibility) { + this.menubarVisibility = visibility; + + if (!skipLayout) { + this.workbenchLayout.layout(); + } + } + } + + getMenubarVisibility(): MenuBarVisibility { + return this.menubarVisibility; + } + getPanelPosition(): Position { return this.panelPosition; } @@ -1441,7 +1496,7 @@ export class Workbench extends Disposable implements IPartService { const newPositionValue = (position === Position.BOTTOM) ? 'bottom' : 'right'; const oldPositionValue = (this.panelPosition === Position.BOTTOM) ? 'bottom' : 'right'; this.panelPosition = position; - this.storageService.store(Workbench.panelPositionStorageKey, Position[this.panelPosition].toLowerCase(), StorageScope.WORKSPACE); + this.storageService.store(Workbench.panelPositionStorageKey, PositionToString(this.panelPosition).toLowerCase(), StorageScope.WORKSPACE); // Adjust CSS DOM.removeClass(this.panelPart.getContainer(), oldPositionValue); @@ -1456,4 +1511,4 @@ export class Workbench extends Disposable implements IPartService { } //#endregion -} \ No newline at end of file +} diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/node/extensionHostMain.ts index 7adcb13e853..8fbb0efc2e2 100644 --- a/src/vs/workbench/node/extensionHostMain.ts +++ b/src/vs/workbench/node/extensionHostMain.ts @@ -13,17 +13,17 @@ import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionS import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { QueryType, ISearchQuery } from 'vs/platform/search/common/search'; -import { DiskSearch } from 'vs/workbench/services/search/node/searchService'; -import { IInitData, IEnvironment, IWorkspaceData, MainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { IInitData, IEnvironment, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from 'vs/workbench/api/node/extHost.protocol'; import * as errors from 'vs/base/common/errors'; -import * as glob from 'vs/base/common/glob'; import { ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; +import { timeout } from 'vs/base/common/async'; +import { Counter } from 'vs/base/common/numbers'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; const nativeExit = process.exit.bind(process); function patchProcess(allowExit: boolean) { @@ -41,8 +41,8 @@ function patchProcess(allowExit: boolean) { console.warn(err.stack); }; } + export function exit(code?: number) { - // TODO@electron // See https://github.com/Microsoft/vscode/issues/32990 // calling process.exit() does not exit the process when the process is being debugged // It waits for the debugger to disconnect, but in our version, the debugger does not @@ -75,8 +75,9 @@ interface ITestRunner { export class ExtensionHostMain { + private static readonly WORKSPACE_CONTAINS_TIMEOUT = 5000; + private _isTerminating: boolean = false; - private _diskSearch: DiskSearch; private _workspace: IWorkspaceData; private _environment: IEnvironment; private _extensionService: ExtHostExtensionService; @@ -84,19 +85,26 @@ export class ExtensionHostMain { private _extHostLogService: ExtHostLogService; private disposables: IDisposable[] = []; + private _searchRequestIdProvider: Counter; + private _mainThreadWorkspace: MainThreadWorkspaceShape; + constructor(protocol: IMessagePassingProtocol, initData: IInitData) { + const rpcProtocol = new RPCProtocol(protocol); + + // ensure URIs are transformed and revived + initData = this.transform(initData, rpcProtocol); this._environment = initData.environment; + this._workspace = initData.workspace; const allowExit = !!this._environment.extensionTestsPath; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) patchProcess(allowExit); // services - const rpcProtocol = new RPCProtocol(protocol); - this._workspace = rpcProtocol.transformIncomingURIs(initData.workspace); - - this._extHostLogService = new ExtHostLogService(initData.windowId, initData.logLevel, initData.logsPath); + this._extHostLogService = new ExtHostLogService(initData.logLevel, initData.logsLocation.fsPath); this.disposables.push(this._extHostLogService); - const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService); + + this._searchRequestIdProvider = new Counter(); + const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService, this._searchRequestIdProvider); this._extHostLogService.info('extension host started'); this._extHostLogService.trace('initData', initData); @@ -124,6 +132,7 @@ export class ExtensionHostMain { return `${error.name || 'Error'}: ${error.message || ''}${stackTraceMessage}`; }; }); + const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService); const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors); errors.setUnexpectedErrorHandler(err => { @@ -136,13 +145,10 @@ export class ExtensionHostMain { } }); - // Configure the watchdog to kill our process if the JS event loop is unresponsive for more than 10s - // if (!initData.environment.isExtensionDevelopmentDebug) { - // watchdog.start(10000); - // } + this._mainThreadWorkspace = rpcProtocol.getProxy(MainContext.MainThreadWorkspace); } - public start(): TPromise { + start(): TPromise { return this._extensionService.onExtensionAPIReady() .then(() => this.handleEagerExtensions()) .then(() => this.handleExtensionTests()) @@ -151,7 +157,7 @@ export class ExtensionHostMain { }); } - public terminate(): void { + terminate(): void { if (this._isTerminating) { // we are already shutting down... return; @@ -166,9 +172,9 @@ export class ExtensionHostMain { let allPromises: TPromise[] = []; try { - let allExtensions = this._extensionService.getAllExtensionDescriptions(); - let allExtensionsIds = allExtensions.map(ext => ext.id); - let activatedExtensions = allExtensionsIds.filter(id => this._extensionService.isActivated(id)); + const allExtensions = this._extensionService.getAllExtensionDescriptions(); + const allExtensionsIds = allExtensions.map(ext => ext.id); + const activatedExtensions = allExtensionsIds.filter(id => this._extensionService.isActivated(id)); allPromises = activatedExtensions.map((extensionId) => { return this._extensionService.deactivate(extensionId); @@ -177,11 +183,11 @@ export class ExtensionHostMain { // TODO: write to log once we have one } - let extensionsDeactivated = TPromise.join(allPromises).then(() => void 0); + const extensionsDeactivated = TPromise.join(allPromises).then(() => void 0); // Give extensions 1 second to wrap up any async dispose, then exit setTimeout(() => { - TPromise.any([TPromise.timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit()); + Promise.race([timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit()); }, 1000); } @@ -190,6 +196,7 @@ export class ExtensionHostMain { this._extensionService.activateByEvent('*', true).then(null, (err) => { console.error(err); }); + return this.handleWorkspaceContainsEagerExtensions(); } @@ -206,7 +213,7 @@ export class ExtensionHostMain { } private handleWorkspaceContainsEagerExtension(desc: IExtensionDescription): TPromise { - let activationEvents = desc.activationEvents; + const activationEvents = desc.activationEvents; if (!activationEvents) { return TPromise.as(void 0); } @@ -216,7 +223,7 @@ export class ExtensionHostMain { for (let i = 0; i < activationEvents.length; i++) { if (/^workspaceContains:/.test(activationEvents[i])) { - let fileNameOrGlob = activationEvents[i].substr('workspaceContains:'.length); + const fileNameOrGlob = activationEvents[i].substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { globPatterns.push(fileNameOrGlob); } else { @@ -229,21 +236,21 @@ export class ExtensionHostMain { return TPromise.as(void 0); } - let fileNamePromise = TPromise.join(fileNames.map((fileName) => this.activateIfFileName(desc.id, fileName))).then(() => { }); - let globPatternPromise = this.activateIfGlobPatterns(desc.id, globPatterns); + const fileNamePromise = TPromise.join(fileNames.map((fileName) => this.activateIfFileName(desc.id, fileName))).then(() => { }); + const globPatternPromise = this.activateIfGlobPatterns(desc.id, globPatterns); return TPromise.join([fileNamePromise, globPatternPromise]).then(() => { }); } - private async activateIfFileName(extensionId: string, fileName: string): TPromise { - // find exact path + private async activateIfFileName(extensionId: string, fileName: string): Promise { + // find exact path for (const { uri } of this._workspace.folders) { if (await pfs.exists(join(URI.revive(uri).fsPath, fileName))) { // the file was found return ( this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${fileName}`)) - .done(null, err => console.error(err)) + .then(null, err => console.error(err)) ); } } @@ -251,43 +258,39 @@ export class ExtensionHostMain { return undefined; } - private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): TPromise { + private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): Promise { this._extHostLogService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId}, entryPoint: workspaceContains`); if (globPatterns.length === 0) { return TPromise.as(void 0); } - if (!this._diskSearch) { - // Shut down this search process after 1s - this._diskSearch = new DiskSearch(false, 1000); + const tokenSource = new CancellationTokenSource(); + const searchP = this._mainThreadWorkspace.$checkExists(globPatterns, tokenSource.token); + + const timer = setTimeout(async () => { + tokenSource.cancel(); + this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContainsTimeout:${globPatterns.join(',')}`)) + .then(null, err => console.error(err)); + }, ExtensionHostMain.WORKSPACE_CONTAINS_TIMEOUT); + + let exists: boolean; + try { + exists = await searchP; + } catch (err) { + if (!errors.isPromiseCanceledError(err)) { + console.error(err); + } } - let includes: glob.IExpression = {}; - globPatterns.forEach((globPattern) => { - includes[globPattern] = true; - }); + tokenSource.dispose(); + clearTimeout(timer); - const folderQueries = this._workspace.folders.map(folder => ({ folder: URI.revive(folder.uri) })); - const config = this._extHostConfiguration.getConfiguration('search'); - const useRipgrep = config.get('useRipgrep', true); - const followSymlinks = config.get('followSymlinks', true); - - const query: ISearchQuery = { - folderQueries, - type: QueryType.File, - exists: true, - includePattern: includes, - useRipgrep, - ignoreSymlinks: !followSymlinks - }; - - let result = await this._diskSearch.search(query); - if (result.limitHit) { + if (exists) { // a file was found matching one of the glob patterns return ( this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${globPatterns.join(',')}`)) - .done(null, err => console.error(err)) + .then(null, err => console.error(err)) ); } @@ -295,7 +298,7 @@ export class ExtensionHostMain { } private handleExtensionTests(): TPromise { - if (!this._environment.extensionTestsPath || !this._environment.extensionDevelopmentPath) { + if (!this._environment.extensionTestsPath || !this._environment.extensionDevelopmentLocationURI) { return TPromise.as(null); } @@ -332,6 +335,16 @@ export class ExtensionHostMain { return TPromise.wrapError(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", this._environment.extensionTestsPath))); } + private transform(initData: IInitData, rpcProtocol: RPCProtocol): IInitData { + initData.extensions.forEach((ext) => (ext).extensionLocation = URI.revive(ext.extensionLocation)); + initData.environment.appRoot = URI.revive(initData.environment.appRoot); + initData.environment.appSettingsHome = URI.revive(initData.environment.appSettingsHome); + initData.environment.extensionDevelopmentLocationURI = URI.revive(initData.environment.extensionDevelopmentLocationURI); + initData.logsLocation = URI.revive(initData.logsLocation); + initData.workspace = rpcProtocol.transformIncomingURIs(initData.workspace); + return initData; + } + private gracefulExit(code: number): void { // to give the PH process a chance to flush any outstanding console // messages to the main process, we delay the exit() by some time diff --git a/src/vs/workbench/node/extensionHostProcess.ts b/src/vs/workbench/node/extensionHostProcess.ts index b91ad78ad4d..f05f72a92a9 100644 --- a/src/vs/workbench/node/extensionHostProcess.ts +++ b/src/vs/workbench/node/extensionHostProcess.ts @@ -8,10 +8,29 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionHostMain, exit } from 'vs/workbench/node/extensionHostMain'; import { IInitData } from 'vs/workbench/api/node/extHost.protocol'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; import { Protocol } from 'vs/base/parts/ipc/node/ipc.net'; import { createConnection } from 'net'; import { Event, filterEvent } from 'vs/base/common/event'; +import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol'; + +// 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 +// https://github.com/electron/electron/issues/10905). To prevent this from +// happening we essentially blocklist this module from getting loaded in any +// extension by patching the node require() function. +(function () { + const Module = require.__$__nodeRequire('module') as any; + const originalLoad = Module._load; + + Module._load = function (request) { + if (request === 'natives') { + throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more'); + } + + return originalLoad.apply(this, arguments); + }; +})(); interface IRendererConnection { protocol: IMessagePassingProtocol; @@ -43,7 +62,7 @@ function createExtHostProtocol(): Promise { private _terminating = false; readonly onMessage: Event = filterEvent(protocol.onMessage, msg => { - if (msg.type !== '__$terminate') { + if (!isMessageOfType(msg, MessageType.Terminate)) { return true; } this._terminating = true; @@ -67,7 +86,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise { first.dispose(); - const initData = JSON.parse(raw); + const initData = JSON.parse(raw.toString()); // Print a console message when rejection isn't handled within N seconds. For details: // see https://nodejs.org/api/process.html#process_event_unhandledrejection @@ -84,6 +103,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise) => { const idx = unhandledPromises.indexOf(promise); if (idx >= 0) { @@ -106,13 +126,13 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise { return extensionHostMain.start(); }).catch(err => console.error(err)); - - function patchExecArgv() { // when encountering the prevent-inspect flag we delete this // and the prior flag diff --git a/src/vs/workbench/parts/backup/common/backupModelTracker.ts b/src/vs/workbench/parts/backup/common/backupModelTracker.ts index 4cb9b8c7670..ab46ef0fde4 100644 --- a/src/vs/workbench/parts/backup/common/backupModelTracker.ts +++ b/src/vs/workbench/parts/backup/common/backupModelTracker.ts @@ -5,10 +5,9 @@ 'use strict'; -import Uri from 'vs/base/common/uri'; -import * as errors from 'vs/base/common/errors'; +import { URI as Uri } from 'vs/base/common/uri'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ITextFileService, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -17,12 +16,11 @@ import { IFilesConfiguration, AutoSaveConfiguration, CONTENT_CHANGE_EVENT_BUFFER const AUTO_SAVE_AFTER_DELAY_DISABLED_TIME = CONTENT_CHANGE_EVENT_BUFFER_DELAY + 500; -export class BackupModelTracker implements IWorkbenchContribution { +export class BackupModelTracker extends Disposable implements IWorkbenchContribution { - public _serviceBrand: any; + _serviceBrand: any; private configuredAutoSaveAfterDelay: boolean; - private toDispose: IDisposable[]; constructor( @IBackupFileService private backupFileService: IBackupFileService, @@ -30,27 +28,24 @@ export class BackupModelTracker implements IWorkbenchContribution { @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IConfigurationService private configurationService: IConfigurationService ) { - this.toDispose = []; + super(); this.registerListeners(); } private registerListeners() { - if (!this.backupFileService.backupEnabled) { - return; - } // Listen for text file model changes - this.toDispose.push(this.textFileService.models.onModelContentChanged((e) => this.onTextFileModelChanged(e))); - this.toDispose.push(this.textFileService.models.onModelSaved((e) => this.discardBackup(e.resource))); - this.toDispose.push(this.textFileService.models.onModelDisposed((e) => this.discardBackup(e))); + this._register(this.textFileService.models.onModelContentChanged((e) => this.onTextFileModelChanged(e))); + this._register(this.textFileService.models.onModelSaved((e) => this.discardBackup(e.resource))); + this._register(this.textFileService.models.onModelDisposed((e) => this.discardBackup(e))); // Listen for untitled model changes - this.toDispose.push(this.untitledEditorService.onDidChangeContent((e) => this.onUntitledModelChanged(e))); - this.toDispose.push(this.untitledEditorService.onDidDisposeModel((e) => this.discardBackup(e))); + this._register(this.untitledEditorService.onDidChangeContent((e) => this.onUntitledModelChanged(e))); + this._register(this.untitledEditorService.onDidDisposeModel((e) => this.discardBackup(e))); // Listen to config changes - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(this.configurationService.getValue()))); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(this.configurationService.getValue()))); } private onConfigurationChange(configuration: IFilesConfiguration): void { @@ -72,24 +67,20 @@ export class BackupModelTracker implements IWorkbenchContribution { // Do not backup when auto save after delay is configured if (!this.configuredAutoSaveAfterDelay) { const model = this.textFileService.models.get(event.resource); - this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()).done(null, errors.onUnexpectedError); + this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()); } } } private onUntitledModelChanged(resource: Uri): void { if (this.untitledEditorService.isDirty(resource)) { - this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.createSnapshot(), model.getVersionId())).done(null, errors.onUnexpectedError); + this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.createSnapshot(), model.getVersionId())); } else { this.discardBackup(resource); } } private discardBackup(resource: Uri): void { - this.backupFileService.discardResourceBackup(resource).done(null, errors.onUnexpectedError); - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); + this.backupFileService.discardResourceBackup(resource); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/backup/common/backupRestorer.ts b/src/vs/workbench/parts/backup/common/backupRestorer.ts index c9fba97e14b..c0ec7e46645 100644 --- a/src/vs/workbench/parts/backup/common/backupRestorer.ts +++ b/src/vs/workbench/parts/backup/common/backupRestorer.ts @@ -5,11 +5,10 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import * as errors from 'vs/base/common/errors'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IResourceInput } from 'vs/platform/editor/common/editor'; @@ -35,11 +34,7 @@ export class BackupRestorer implements IWorkbenchContribution { } private restoreBackups(): void { - if (this.backupFileService.backupEnabled) { - this.lifecycleService.when(LifecyclePhase.Running).then(() => { - this.doRestoreBackups().done(null, errors.onUnexpectedError); - }); - } + this.lifecycleService.when(LifecyclePhase.Running).then(() => this.doRestoreBackups()); } private doRestoreBackups(): TPromise { diff --git a/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts b/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts index 5e754fb953d..29c3894ea60 100644 --- a/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts +++ b/src/vs/workbench/parts/cli/electron-browser/cli.contribution.ts @@ -10,7 +10,6 @@ import * as pfs from 'vs/base/node/pfs'; import * as platform from 'vs/base/common/platform'; import { nfcall } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -20,6 +19,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { ILogService } from 'vs/platform/log/common/log'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; function ignore(code: string, value: T = null): (err: any) => TPromise { return err => err.code === code ? TPromise.as(value) : TPromise.wrapError(err); @@ -28,7 +28,7 @@ function ignore(code: string, value: T = null): (err: any) => TPromise { let _source: string = null; function getSource(): string { if (!_source) { - const root = URI.parse(require.toUrl('')).fsPath; + const root = getPathFromAmdModule(require, ''); _source = path.resolve(root, '..', 'bin', 'code'); } return _source; @@ -70,20 +70,16 @@ class InstallAction extends Action { if (!isAvailable || isInstalled) { return TPromise.as(null); } else { - const createSymlink = () => { - return pfs.unlink(this.target) - .then(null, ignore('ENOENT')) - .then(() => pfs.symlink(getSource(), this.target)); - }; + return pfs.unlink(this.target) + .then(null, ignore('ENOENT')) + .then(() => pfs.symlink(getSource(), this.target)) + .then(null, err => { + if (err.code === 'EACCES' || err.code === 'ENOENT') { + return this.createBinFolderAndSymlinkAsAdmin(); + } - return createSymlink().then(null, err => { - if (err.code === 'EACCES' || err.code === 'ENOENT') { - return this.createBinFolder() - .then(() => createSymlink()); - } - - return TPromise.wrapError(err); - }); + return TPromise.wrapError(err); + }); } }) .then(() => { @@ -101,18 +97,18 @@ class InstallAction extends Action { .then(null, ignore('ENOENT', false)); } - private createBinFolder(): TPromise { + private createBinFolderAndSymlinkAsAdmin(): TPromise { return new TPromise((c, e) => { const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; this.dialogService.show(Severity.Info, nls.localize('warnEscalation', "Code will now prompt with 'osascript' for Administrator privileges to install the shell command."), buttons, { cancelId: 1 }).then(choice => { switch (choice) { case 0 /* OK */: - const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && chown \\" & (do shell script (\\"whoami\\")) & \\" /usr/local/bin\\" with administrator privileges"'; + const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + this.target + '\'\\" with administrator privileges"'; nfcall(cp.exec, command, {}) .then(null, _ => TPromise.wrapError(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) - .done(c, e); + .then(c, e); break; case 1 /* Cancel */: e(new Error(nls.localize('aborted', "Aborted"))); @@ -132,7 +128,8 @@ class UninstallAction extends Action { id: string, label: string, @INotificationService private notificationService: INotificationService, - @ILogService private logService: ILogService + @ILogService private logService: ILogService, + @IDialogService private dialogService: IDialogService ) { super(id, label); } @@ -149,12 +146,42 @@ class UninstallAction extends Action { return undefined; } - return pfs.unlink(this.target) - .then(null, ignore('ENOENT')) - .then(() => { - this.logService.trace('cli#uninstall', this.target); - this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", product.applicationName)); - }); + const uninstall = () => { + return pfs.unlink(this.target) + .then(null, ignore('ENOENT')); + }; + + return uninstall().then(null, err => { + if (err.code === 'EACCES') { + return this.deleteSymlinkAsAdmin(); + } + + return TPromise.wrapError(err); + }).then(() => { + this.logService.trace('cli#uninstall', this.target); + this.notificationService.info(nls.localize('successFrom', "Shell command '{0}' successfully uninstalled from PATH.", product.applicationName)); + }); + }); + } + + private deleteSymlinkAsAdmin(): TPromise { + return new TPromise((c, e) => { + const buttons = [nls.localize('ok', "OK"), nls.localize('cancel2', "Cancel")]; + + this.dialogService.show(Severity.Info, nls.localize('warnEscalationUninstall', "Code will now prompt with 'osascript' for Administrator privileges to uninstall the shell command."), buttons, { cancelId: 1 }).then(choice => { + switch (choice) { + case 0 /* OK */: + const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"'; + + nfcall(cp.exec, command, {}) + .then(null, _ => TPromise.wrapError(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", this.target)))) + .then(c, e); + break; + case 1 /* Cancel */: + e(new Error(nls.localize('aborted', "Aborted"))); + break; + } + }); }); } } diff --git a/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css b/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css new file mode 100644 index 00000000000..65fd4a3c8ba --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.suggest-input-container { + padding: 3px 4px 5px; +} + +.suggest-input-container .monaco-editor-background, +.suggest-input-container .monaco-editor, +.suggest-input-container .mtk1 { + /* allow the embedded monaco to be styled from the outer context */ + background-color: transparent; + color: inherit; +} + +.suggest-input-container .suggest-input-placeholder { + position: absolute; + z-index: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + margin-top: 2px; + margin-left: 1px; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/menuPreventer.ts b/src/vs/workbench/parts/codeEditor/browser/menuPreventer.ts similarity index 100% rename from src/vs/workbench/parts/codeEditor/electron-browser/menuPreventer.ts rename to src/vs/workbench/parts/codeEditor/browser/menuPreventer.ts diff --git a/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts new file mode 100644 index 00000000000..a18c1f0479d --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; + +export function getSimpleEditorOptions(): IEditorOptions { + return { + wordWrap: 'on', + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + selectOnLineNumbers: false, + hideCursorInOverviewRuler: true, + selectionHighlight: false, + scrollbar: { + horizontal: 'hidden' + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + acceptSuggestionOnEnter: 'smart', + minimap: { + enabled: false + } + }; +} diff --git a/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts b/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts new file mode 100644 index 00000000000..62d86f7f955 --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { $, addClass, append, Dimension, removeClass } from 'vs/base/browser/dom'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { Color } from 'vs/base/common/color'; +import { chain, Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import { URI as uri } from 'vs/base/common/uri'; +import 'vs/css!./media/suggestEnabledInput'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import * as modes from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ColorIdentifier, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler, IStyleOverrides, IThemable } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; + +interface SuggestResultsProvider { + /** + * Provider function for suggestion results. + * + * @param query the full text of the input. + */ + provideResults: (query: string) => string[]; + + /** + * Trigger characters for this input. Suggestions will appear when one of these is typed, + * or upon `ctrl+space` triggering at a word boundary. + * + * Defaults to the empty array. + */ + triggerCharacters?: string[]; + + /** + * Defines the sorting function used when showing results. + * + * Defaults to the identity function. + */ + sortKey?: (result: string) => string; +} + +interface SuggestEnabledInputOptions { + /** + * The text to show when no input is present. + * + * Defaults to the empty string. + */ + placeholderText?: string; + + /** + * Context key tracking the focus state of this element + */ + focusContextKey?: IContextKey; +} + +export interface ISuggestEnabledInputStyleOverrides extends IStyleOverrides { + inputBackground?: ColorIdentifier; + inputForeground?: ColorIdentifier; + inputBorder?: ColorIdentifier; + inputPlaceholderForeground?: ColorIdentifier; +} + +type ISuggestEnabledInputStyles = { + [P in keyof ISuggestEnabledInputStyleOverrides]: Color; +}; + + +export function attachSuggestEnabledInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: ISuggestEnabledInputStyleOverrides): IDisposable { + return attachStyler(themeService, { + inputBackground: (style && style.inputBackground) || inputBackground, + inputForeground: (style && style.inputForeground) || inputForeground, + inputBorder: (style && style.inputBorder) || inputBorder, + inputPlaceholderForeground: (style && style.inputPlaceholderForeground) || inputPlaceholderForeground, + } as ISuggestEnabledInputStyleOverrides, widget); +} + + +export class SuggestEnabledInput extends Widget implements IThemable { + + private _onShouldFocusResults = new Emitter(); + readonly onShouldFocusResults: Event = this._onShouldFocusResults.event; + + private _onEnter = new Emitter(); + readonly onEnter: Event = this._onEnter.event; + + private _onInputDidChange = new Emitter(); + readonly onInputDidChange: Event = this._onInputDidChange.event; + + private disposables: IDisposable[] = []; + private inputWidget: CodeEditorWidget; + private stylingContainer: HTMLDivElement; + private placeholderText: HTMLDivElement; + + constructor( + id: string, + parent: HTMLElement, + suggestionProvider: SuggestResultsProvider, + ariaLabel: string, + resourceHandle: string, + options: SuggestEnabledInputOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + ) { + super(); + + this.stylingContainer = append(parent, $('.suggest-input-container')); + this.placeholderText = append(this.stylingContainer, $('.suggest-input-placeholder', null, options.placeholderText || '')); + + this.inputWidget = instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, + mixinHTMLInputStyleOptions(getSimpleEditorOptions(), ariaLabel), + { + contributions: [SuggestController, SnippetController2, ContextMenuController, MenuPreventer], + isSimpleWidget: true, + }); + + let scopeHandle = uri.parse(resourceHandle); + this.inputWidget.setModel(modelService.createModel('', null, scopeHandle, true)); + + this.disposables.push(this.inputWidget.onDidPaste(() => this.setValue(this.getValue()))); // setter cleanses + + this.disposables.push((this.inputWidget.onDidFocusEditorText(() => { + if (options.focusContextKey) { options.focusContextKey.set(true); } + addClass(this.stylingContainer, 'synthetic-focus'); + }))); + this.disposables.push((this.inputWidget.onDidBlurEditorText(() => { + if (options.focusContextKey) { options.focusContextKey.set(false); } + removeClass(this.stylingContainer, 'synthetic-focus'); + }))); + + const onKeyDownMonaco = chain(this.inputWidget.onKeyDown); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => { e.preventDefault(); this._onEnter.fire(); }, this, this.disposables); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow && (isMacintosh ? e.metaKey : e.ctrlKey)).on(() => this._onShouldFocusResults.fire(), this, this.disposables); + + let preexistingContent = this.getValue(); + this.disposables.push(this.inputWidget.getModel().onDidChangeContent(() => { + let content = this.getValue(); + this.placeholderText.style.visibility = content ? 'hidden' : 'visible'; + if (preexistingContent.trim() === content.trim()) { return; } + this._onInputDidChange.fire(); + preexistingContent = content; + })); + + let validatedSuggestProvider = { + provideResults: suggestionProvider.provideResults, + sortKey: suggestionProvider.sortKey || (a => a), + triggerCharacters: suggestionProvider.triggerCharacters || [] + }; + + this.disposables.push(modes.SuggestRegistry.register({ scheme: scopeHandle.scheme, pattern: '**/' + scopeHandle.path, hasAccessToAllModels: true }, { + triggerCharacters: validatedSuggestProvider.triggerCharacters, + provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext) => { + let query = model.getValue(); + + let wordStart = query.lastIndexOf(' ', position.column - 1) + 1; + let alreadyTypedCount = position.column - wordStart - 1; + + // dont show suggestions if the user has typed something, but hasn't used the trigger character + if (alreadyTypedCount > 0 && (validatedSuggestProvider.triggerCharacters).indexOf(query[wordStart]) === -1) { return { suggestions: [] }; } + + return { + suggestions: suggestionProvider.provideResults(query).map(result => { + return { + label: result, + insertText: result, + overwriteBefore: alreadyTypedCount, + sortText: validatedSuggestProvider.sortKey(result), + type: 'keyword' + }; + }) + }; + } + })); + } + + public setValue(val: string) { + val = val.replace(/\s/g, ' '); + const fullRange = new Range(1, 1, 1, this.getValue().length + 1); + this.inputWidget.executeEdits('suggestEnabledInput.setValue', [EditOperation.replace(fullRange, val)]); + this.inputWidget.setScrollTop(0); + this.inputWidget.setPosition(new Position(1, val.length + 1)); + } + + public getValue(): string { + return this.inputWidget.getValue(); + } + + + public style(colors: ISuggestEnabledInputStyles): void { + this.stylingContainer.style.backgroundColor = colors.inputBackground && colors.inputBackground.toString(); + this.stylingContainer.style.color = colors.inputForeground && colors.inputForeground.toString(); + this.placeholderText.style.color = colors.inputPlaceholderForeground && colors.inputPlaceholderForeground.toString(); + + this.stylingContainer.style.borderWidth = '1px'; + this.stylingContainer.style.borderStyle = 'solid'; + this.stylingContainer.style.borderColor = colors.inputBorder ? + colors.inputBorder.toString() : + 'transparent'; + + const cursor = this.stylingContainer.getElementsByClassName('cursor')[0] as HTMLDivElement; + if (cursor) { + cursor.style.backgroundColor = colors.inputForeground && colors.inputForeground.toString(); + } + } + + public focus(selectAll?: boolean): void { + this.inputWidget.focus(); + + if (selectAll && this.inputWidget.getValue()) { + this.selectAll(); + } + } + + public layout(dimension: Dimension): void { + this.inputWidget.layout(dimension); + this.placeholderText.style.width = `${dimension.width}px`; + } + + private selectAll(): void { + this.inputWidget.setSelection(new Range(1, 1, 1, this.getValue().length + 1)); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + + +function mixinHTMLInputStyleOptions(config: IEditorOptions, ariaLabel?: string): IEditorOptions { + config.fontSize = 13; + config.lineHeight = 22; + config.wordWrap = 'off'; + config.scrollbar.vertical = 'hidden'; + config.roundedSelection = false; + config.ariaLabel = ariaLabel || ''; + config.renderIndentGuides = false; + config.cursorWidth = 1; + config.snippetSuggestions = 'none'; + config.suggest = { filterGraceful: false }; + config.fontFamily = ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif'; + return config; +} diff --git a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts index cc4c07f981a..d66877173a1 100644 --- a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts +++ b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts @@ -6,7 +6,7 @@ import './electron-browser/accessibility'; import './electron-browser/inspectKeybindings'; import './electron-browser/largeFileOptimizations'; -import './electron-browser/menuPreventer'; +import './browser/menuPreventer'; import './electron-browser/selectionClipboard'; import './electron-browser/textMate/inspectTMScopes'; import './electron-browser/toggleMinimap'; diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts b/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts index a9e4ec6741e..ab344b9b990 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/accessibility.ts @@ -29,8 +29,8 @@ import * as editorOptions from 'vs/editor/common/config/editorOptions'; import * as platform from 'vs/base/common/platform'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import URI from 'vs/base/common/uri'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { URI } from 'vs/base/common/uri'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey('accessibilityHelpWidgetVisible', false); @@ -286,7 +286,8 @@ class ShowAccessibilityHelpAction extends EditorAction { precondition: null, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.Alt | KeyCode.F1 + primary: KeyMod.Alt | KeyCode.F1, + weight: KeybindingWeight.EditorContrib } }); } @@ -309,7 +310,7 @@ registerEditorCommand(new AccessibilityHelpCommand({ precondition: CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE, handler: x => x.hide(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(100), + weight: KeybindingWeight.EditorContrib + 100, kbExpr: EditorContextKeys.focus, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] } diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts b/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts index 5f5612b0a5e..a7f79a6be50 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { parse, ParseError } from 'vs/base/common/json'; -import { readFile } from 'vs/base/node/pfs'; import { CharacterPair, LanguageConfiguration, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, CommentRule, FoldingRules } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -16,6 +15,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextMateService } from 'vs/workbench/services/textMate/electron-browser/textMateService'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; interface IRegExp { pattern: string; @@ -37,6 +38,7 @@ interface ILanguageConfiguration { wordPattern?: string | IRegExp; indentationRules?: IIndentationRules; folding?: FoldingRules; + autoCloseBefore?: string; } function isStringArr(something: string[]): boolean { @@ -61,14 +63,13 @@ function isCharacterPair(something: CharacterPair): boolean { export class LanguageConfigurationFileHandler { - private _modeService: IModeService; private _done: boolean[]; constructor( @ITextMateService textMateService: ITextMateService, - @IModeService modeService: IModeService + @IModeService private readonly _modeService: IModeService, + @IFileService private readonly _fileService: IFileService ) { - this._modeService = modeService; this._done = []; // Listen for hints that a language configuration is needed/usefull and then load it once @@ -85,15 +86,15 @@ export class LanguageConfigurationFileHandler { this._done[languageIdentifier.id] = true; let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); - configurationFiles.forEach((configFilePath) => this._handleConfigFile(languageIdentifier, configFilePath)); + configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation)); } - private _handleConfigFile(languageIdentifier: LanguageIdentifier, configFilePath: string): void { - readFile(configFilePath).then((fileContents) => { + private _handleConfigFile(languageIdentifier: LanguageIdentifier, configFileLocation: URI): void { + this._fileService.resolveContent(configFileLocation, { encoding: 'utf8' }).then((contents) => { const errors: ParseError[] = []; - const configuration = parse(fileContents.toString(), errors); + const configuration = parse(contents.value.toString(), errors); if (errors.length) { - console.error(nls.localize('parseErrors', "Errors parsing {0}: {1}", configFilePath, errors.join('\n'))); + console.error(nls.localize('parseErrors', "Errors parsing {0}: {1}", configFileLocation.toString(), errors.join('\n'))); } this._handleConfig(languageIdentifier, configuration); }, (err) => { @@ -274,6 +275,11 @@ export class LanguageConfigurationFileHandler { richEditConfig.surroundingPairs = surroundingPairs; } + const autoCloseBefore = configuration.autoCloseBefore; + if (typeof autoCloseBefore === 'string') { + richEditConfig.autoCloseBefore = autoCloseBefore; + } + if (configuration.wordPattern) { try { let wordPattern = this._parseRegex(configuration.wordPattern); @@ -433,6 +439,11 @@ const schema: IJSONSchema = { }] } }, + autoCloseBefore: { + default: ';:.,=}])> \n\t', + description: nls.localize('schema.autoCloseBefore', 'Defines what characters must be after the cursor in order for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. This is typically the set of characters which can not start an expression.'), + type: 'string', + }, surroundingPairs: { default: [['(', ')'], ['[', ']'], ['{', '}']], description: nls.localize('schema.surroundingPairs', 'Defines the bracket pairs that can be used to surround a selected string.'), diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts b/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts new file mode 100644 index 00000000000..4e3a0f1a14c --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; +import { SelectionClipboard } from 'vs/workbench/parts/codeEditor/electron-browser/selectionClipboard'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { TabCompletionController } from 'vs/workbench/parts/snippets/electron-browser/tabCompletion'; + +export function getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { + isSimpleWidget: true, + contributions: [ + MenuPreventer, + SelectionClipboard, + ContextMenuController, + SuggestController, + SnippetController2, + TabCompletionController, + ] + }; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts index 8361e7b855b..36617f7b8c3 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMinimap.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleMinimapAction extends Action { public static readonly ID = 'editor.action.toggleMinimap'; @@ -32,3 +32,12 @@ export class ToggleMinimapAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMinimapAction, ToggleMinimapAction.ID, ToggleMinimapAction.LABEL), 'View: Toggle Minimap'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleMinimapAction.ID, + title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleMultiCursorModifier.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMultiCursorModifier.ts index e7b79739e01..608554450f8 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleMultiCursorModifier.ts @@ -6,11 +6,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import * as platform from 'vs/base/common/platform'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/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 { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; export class ToggleMultiCursorModifierAction extends Action { @@ -35,5 +39,55 @@ export class ToggleMultiCursorModifierAction extends Action { } } +const multiCursorModifier = new RawContextKey('multiCursorModifier', 'altKey'); + +class MultiCursorModifierContextKeyController implements IWorkbenchContribution { + + private readonly _multiCursorModifier: IContextKey; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + this._multiCursorModifier = multiCursorModifier.bindTo(contextKeyService); + configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('editor.multiCursorModifier')) { + this._update(); + } + }); + } + + private _update(): void { + const editorConf = this.configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); + const value = (editorConf.multiCursorModifier === 'ctrlCmd' ? 'ctrlCmd' : 'altKey'); + this._multiCursorModifier.set(value); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(MultiCursorModifierContextKeyController, LifecyclePhase.Running); + + const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleMultiCursorModifierAction, ToggleMultiCursorModifierAction.ID, ToggleMultiCursorModifierAction.LABEL), 'Toggle Multi-Cursor Modifier'); +MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: ToggleMultiCursorModifierAction.ID, + title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") + }, + when: multiCursorModifier.isEqualTo('ctrlCmd'), + order: 1 +}); +MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: ToggleMultiCursorModifierAction.ID, + title: ( + platform.isMacintosh + ? nls.localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") + : nls.localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor") + ) + }, + when: multiCursorModifier.isEqualTo('altKey'), + order: 1 +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts index 7c35e71b00c..42c4e6bf068 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderControlCharacter.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleRenderControlCharacterAction extends Action { @@ -33,3 +33,12 @@ export class ToggleRenderControlCharacterAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderControlCharacterAction, ToggleRenderControlCharacterAction.ID, ToggleRenderControlCharacterAction.LABEL), 'View: Toggle Control Characters'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleRenderControlCharacterAction.ID, + title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters") + }, + order: 4 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts index 856e35dce60..d9a61a863eb 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleRenderWhitespace.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; export class ToggleRenderWhitespaceAction extends Action { @@ -41,3 +41,12 @@ export class ToggleRenderWhitespaceAction extends Action { const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleRenderWhitespaceAction, ToggleRenderWhitespaceAction.ID, ToggleRenderWhitespaceAction.LABEL), 'View: Toggle Render Whitespace'); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: ToggleRenderWhitespaceAction.ID, + title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace") + }, + order: 3 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts index 96f4858f078..bea4e2e3faa 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts @@ -13,11 +13,12 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Disposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { InternalEditorOptions, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; const transientWordWrapState = 'transientWordWrapState'; const isWordWrapMinifiedKey = 'isWordWrapMinified'; @@ -130,17 +131,19 @@ function applyWordWrapState(editor: ICodeEditor, state: IWordWrapState): void { }); } +const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap'; class ToggleWordWrapAction extends EditorAction { constructor() { super({ - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, label: nls.localize('toggle.wordwrap', "View: Toggle Word Wrap"), alias: 'View: Toggle Word Wrap', precondition: null, kbOpts: { kbExpr: null, - primary: KeyMod.Alt | KeyCode.KEY_Z + primary: KeyMod.Alt | KeyCode.KEY_Z, + weight: KeybindingWeight.EditorContrib } }); } @@ -255,9 +258,9 @@ registerEditorAction(ToggleWordWrapAction); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, title: nls.localize('unwrapMinified', "Disable wrapping for this file"), - iconPath: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')).fsPath } + iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')) } }, group: 'navigation', order: 1, @@ -269,9 +272,9 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'editor.action.toggleWordWrap', + id: TOGGLE_WORD_WRAP_ID, title: nls.localize('wrapMinified', "Enable wrapping for this file"), - iconPath: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')).fsPath } + iconLocation: { dark: URI.parse(require.toUrl('vs/workbench/parts/codeEditor/electron-browser/media/WordWrap_16x.svg')) } }, group: 'navigation', order: 1, @@ -281,3 +284,14 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { ContextKeyExpr.not(isWordWrapMinifiedKey) ) }); + + +// View menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '5_editor', + command: { + id: TOGGLE_WORD_WRAP_ID, + title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts b/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts index 8155d1693a0..260c669fa73 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/workbenchReferenceSearch.ts @@ -4,128 +4,38 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { IInstantiationService, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ReferencesController } from 'vs/editor/contrib/referenceSearch/referencesController'; -import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/referencesModel'; -import { Range } from 'vs/editor/common/core/range'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import URI from 'vs/base/common/uri'; -import { Location } from 'vs/editor/common/modes'; -import { provideReferences, defaultReferenceSearchOptions } from 'vs/editor/contrib/referenceSearch/referenceSearch'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class WorkbenchReferencesController extends ReferencesController { public constructor( editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, - @ICodeEditorService codeEditorService: ICodeEditorService, - @ITextModelService textModelResolverService: ITextModelService, + @ICodeEditorService editorService: ICodeEditorService, @INotificationService notificationService: INotificationService, @IInstantiationService instantiationService: IInstantiationService, - @IWorkspaceContextService contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @optional(IEnvironmentService) environmentService: IEnvironmentService ) { super( false, editor, contextKeyService, - codeEditorService, - textModelResolverService, + editorService, notificationService, instantiationService, - contextService, storageService, - themeService, configurationService, - environmentService ); } } registerEditorContribution(WorkbenchReferencesController); - -let findReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resource: URI, position: IPosition) => { - - if (!(resource instanceof URI)) { - throw new Error('illegal argument, uri'); - } - if (!position) { - throw new Error('illegal argument, position'); - } - - return accessor.get(IEditorService).openEditor({ resource }).then(editor => { - let control = editor.getControl(); - if (!isCodeEditor(control)) { - return undefined; - } - - let controller = ReferencesController.get(control); - if (!controller) { - return undefined; - } - - let references = provideReferences(control.getModel(), Position.lift(position)).then(references => new ReferencesModel(references)); - let range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - return TPromise.as(controller.toggleWidget(range, references, defaultReferenceSearchOptions)); - }); -}; - -let showReferencesCommand: ICommandHandler = (accessor: ServicesAccessor, resource: URI, position: IPosition, references: Location[]) => { - if (!(resource instanceof URI)) { - throw new Error('illegal argument, uri expected'); - } - - return accessor.get(IEditorService).openEditor({ resource }).then(editor => { - let control = editor.getControl(); - if (!isCodeEditor(control)) { - return undefined; - } - - let controller = ReferencesController.get(control); - if (!controller) { - return undefined; - } - - return TPromise.as(controller.toggleWidget( - new Range(position.lineNumber, position.column, position.lineNumber, position.column), - TPromise.as(new ReferencesModel(references)), - defaultReferenceSearchOptions)).then(() => true); - }); -}; - -// register commands - -CommandsRegistry.registerCommand({ - id: 'editor.action.findReferences', - handler: findReferencesCommand -}); - -CommandsRegistry.registerCommand({ - id: 'editor.action.showReferences', - handler: showReferencesCommand, - description: { - description: 'Show references at a position in a file', - args: [ - { name: 'uri', description: 'The text document in which to show references', constraint: URI }, - { name: 'position', description: 'The position at which to show', constraint: Position.isIPosition }, - { name: 'locations', description: 'An array of locations.', constraint: Array }, - ] - } -}); diff --git a/src/vs/workbench/parts/comments/common/commentModel.ts b/src/vs/workbench/parts/comments/common/commentModel.ts index 2cf98fa3dcb..a1fde2ad8bb 100644 --- a/src/vs/workbench/parts/comments/common/commentModel.ts +++ b/src/vs/workbench/parts/comments/common/commentModel.ts @@ -5,11 +5,12 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/modes'; -import { groupBy, firstIndex } from 'vs/base/common/arrays'; +import { groupBy, firstIndex, flatten } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; +import { values } from 'vs/base/common/map'; export class CommentNode { threadId: string; @@ -54,21 +55,30 @@ export class ResourceWithCommentThreads { export class CommentsModel { resourceCommentThreads: ResourceWithCommentThreads[]; + commentThreadsMap: Map; constructor() { this.resourceCommentThreads = []; + this.commentThreadsMap = new Map(); } - public setCommentThreads(commentThreads: CommentThread[]): void { - this.resourceCommentThreads = []; - this.addCommentThreads(commentThreads); + public setCommentThreads(owner: number, commentThreads: CommentThread[]): void { + this.commentThreadsMap.set(owner, this.groupByResource(commentThreads)); + this.resourceCommentThreads = flatten(values(this.commentThreadsMap)); } - public updateCommentThreads(event: CommentThreadChangedEvent): void { - event.removed.forEach(thread => { + public updateCommentThreads(event: CommentThreadChangedEvent): boolean { + const { owner, removed, changed, added } = event; + if (!this.commentThreadsMap.has(owner)) { + return false; + } + + let threadsForOwner = this.commentThreadsMap.get(owner); + + removed.forEach(thread => { // Find resource that has the comment thread - const matchingResourceIndex = firstIndex(this.resourceCommentThreads, (resourceData) => resourceData.id === thread.resource); - const matchingResourceData = this.resourceCommentThreads[matchingResourceIndex]; + const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource); + const matchingResourceData = threadsForOwner[matchingResourceIndex]; // Find comment node on resource that is that thread and remove it const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId); @@ -76,21 +86,34 @@ export class CommentsModel { // If the comment thread was the last thread for a resource, remove that resource from the list if (matchingResourceData.commentThreads.length === 0) { - this.resourceCommentThreads.splice(matchingResourceIndex, 1); + threadsForOwner.splice(matchingResourceIndex, 1); } }); - event.changed.forEach(thread => { + changed.forEach(thread => { // Find resource that has the comment thread - const matchingResourceIndex = firstIndex(this.resourceCommentThreads, (resourceData) => resourceData.id === thread.resource); - const matchingResourceData = this.resourceCommentThreads[matchingResourceIndex]; + const matchingResourceIndex = firstIndex(threadsForOwner, (resourceData) => resourceData.id === thread.resource); + const matchingResourceData = threadsForOwner[matchingResourceIndex]; // Find comment node on resource that is that thread and replace it const index = firstIndex(matchingResourceData.commentThreads, (commentThread) => commentThread.threadId === thread.threadId); matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(URI.parse(matchingResourceData.id), thread); }); - this.addCommentThreads(event.added); + added.forEach(thread => { + const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource); + if (existingResource.length) { + const resource = existingResource[0]; + resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(resource.resource, thread)); + } else { + threadsForOwner.push(new ResourceWithCommentThreads(URI.parse(thread.resource), [thread])); + } + }); + + this.commentThreadsMap.set(owner, threadsForOwner); + this.resourceCommentThreads = flatten(values(this.commentThreadsMap)); + + return removed.length > 0 || changed.length > 0 || added.length > 0; } public hasCommentThreads(): boolean { @@ -105,15 +128,18 @@ export class CommentsModel { } } - private addCommentThreads(commentThreads: CommentThread[]): void { + private groupByResource(commentThreads: CommentThread[]): ResourceWithCommentThreads[] { + const resourceCommentThreads = []; const commentThreadsByResource = new Map(); for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) { commentThreadsByResource.set(group[0].resource, new ResourceWithCommentThreads(URI.parse(group[0].resource), group)); } commentThreadsByResource.forEach((v, i, m) => { - this.resourceCommentThreads.push(v); + resourceCommentThreads.push(v); }); + + return resourceCommentThreads; } private static _compareURIs(a: CommentThread, b: CommentThread) { diff --git a/src/vs/workbench/parts/comments/common/reviewModel.ts b/src/vs/workbench/parts/comments/common/reviewModel.ts deleted file mode 100644 index 2c3b962122f..00000000000 --- a/src/vs/workbench/parts/comments/common/reviewModel.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { Emitter, Event } from 'vs/base/common/event'; - -export enum ReviewStyle { - Complete, - Inline, - Gutter -} - -export class ReviewModel { - private _style: ReviewStyle; - public get style(): ReviewStyle { return this._style; } - private _onDidChangeStyle = new Emitter(); - public get onDidChangeStyle(): Event { return this._onDidChangeStyle.event; } - - constructor() { - this._style = ReviewStyle.Inline; - } - - setStyle(style: ReviewStyle) { - this._style = style; - this._onDidChangeStyle.fire(this._style); - } -} \ 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 44d7a10810e..379481a63ed 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentGlyphWidget.ts @@ -4,37 +4,38 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -export class CommentGlyphWidget implements IContentWidget { - private _id: string; +export class CommentGlyphWidget { private _lineNumber: number; - private _domNode: HTMLDivElement; private _editor: ICodeEditor; + private commentsDecorations: string[] = []; + private _commentsOptions: ModelDecorationOptions; - constructor(id: string, editor: ICodeEditor, lineNumber: number, disabled: boolean, onClick: () => void) { - this._id = id; - this._domNode = document.createElement('div'); - this._domNode.className = disabled ? 'comment-hint commenting-disabled' : 'comment-hint'; - this._domNode.addEventListener('click', onClick); + constructor(editor: ICodeEditor, lineNumber: number, commentsOptions: ModelDecorationOptions, onClick: () => void) { + this._commentsOptions = commentsOptions; this._lineNumber = lineNumber; - this._editor = editor; - this._editor.addContentWidget(this); - + this.update(); } - getDomNode(): HTMLElement { - return this._domNode; - } + update() { + let commentsDecorations = [{ + range: { + startLineNumber: this._lineNumber, startColumn: 1, + endLineNumber: this._lineNumber, endColumn: 1 + }, + options: this._commentsOptions + }]; - getId(): string { - return this._id; + this.commentsDecorations = this._editor.deltaDecorations(this.commentsDecorations, commentsDecorations); } setLineNumber(lineNumber: number): void { this._lineNumber = lineNumber; + this.update(); } getPosition(): IContentWidgetPosition { @@ -46,4 +47,10 @@ export class CommentGlyphWidget implements IContentWidget { preference: [ContentWidgetPositionPreference.EXACT] }; } + + dispose() { + if (this.commentsDecorations) { + this._editor.deltaDecorations(this.commentsDecorations, []); + } + } } \ No newline at end of file diff --git a/src/vs/workbench/parts/comments/electron-browser/commentService.ts b/src/vs/workbench/parts/comments/electron-browser/commentService.ts index 427fbb6f338..c458cce2bd8 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentService.ts @@ -5,15 +5,14 @@ 'use strict'; -import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, WorkspaceCommentProvider } from 'vs/editor/common/modes'; +import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo } 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'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { asWinJsPromise } from 'vs/base/common/async'; import { keys } from 'vs/base/common/map'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const ICommentService = createDecorator('commentService'); @@ -22,22 +21,27 @@ export interface IResourceCommentThreadEvent { commentInfos: CommentInfo[]; } +export interface IWorkspaceCommentThreadsEvent { + ownerId: number; + commentThreads: CommentThread[]; +} + export interface ICommentService { _serviceBrand: any; readonly onDidSetResourceCommentInfos: Event; - readonly onDidSetAllCommentThreads: Event; + readonly onDidSetAllCommentThreads: Event; readonly onDidUpdateCommentThreads: Event; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; - setComments(resource: URI, commentInfos: CommentInfo[]): void; - setAllComments(commentsByResource: CommentThread[]): void; - removeAllComments(): void; - registerDataProvider(owner: number, commentProvider: DocumentCommentProvider | WorkspaceCommentProvider): void; + setDocumentComments(resource: URI, commentInfos: CommentInfo[]): void; + setWorkspaceComments(owner: number, commentsByResource: CommentThread[]): void; + removeWorkspaceComments(owner: number): void; + registerDataProvider(owner: number, commentProvider: DocumentCommentProvider): void; unregisterDataProvider(owner: number): void; updateComments(event: CommentThreadChangedEvent): void; - createNewCommenThread(owner: number, resource: URI, range: Range, text: string): TPromise; - replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): TPromise; - getComments(resource: URI): TPromise; + createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise; + replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise; + getComments(resource: URI): Promise; } export class CommentService extends Disposable implements ICommentService { @@ -52,31 +56,31 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidSetResourceCommentInfos: Emitter = this._register(new Emitter()); readonly onDidSetResourceCommentInfos: Event = this._onDidSetResourceCommentInfos.event; - private readonly _onDidSetAllCommentThreads: Emitter = this._register(new Emitter()); - readonly onDidSetAllCommentThreads: Event = this._onDidSetAllCommentThreads.event; + private readonly _onDidSetAllCommentThreads: Emitter = this._register(new Emitter()); + readonly onDidSetAllCommentThreads: Event = this._onDidSetAllCommentThreads.event; private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; - private _commentProviders = new Map(); + private _commentProviders = new Map(); constructor() { super(); } - setComments(resource: URI, commentInfos: CommentInfo[]): void { + setDocumentComments(resource: URI, commentInfos: CommentInfo[]): void { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } - setAllComments(commentsByResource: CommentThread[]): void { - this._onDidSetAllCommentThreads.fire(commentsByResource); + setWorkspaceComments(owner: number, commentsByResource: CommentThread[]): void { + this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: commentsByResource }); } - removeAllComments(): void { - this._onDidSetAllCommentThreads.fire([]); + removeWorkspaceComments(owner: number): void { + this._onDidSetAllCommentThreads.fire({ ownerId: owner, commentThreads: [] }); } - registerDataProvider(owner: number, commentProvider: DocumentCommentProvider | WorkspaceCommentProvider) { + registerDataProvider(owner: number, commentProvider: DocumentCommentProvider) { this._commentProviders.set(owner, commentProvider); this._onDidSetDataProvider.fire(); } @@ -90,35 +94,35 @@ export class CommentService extends Disposable implements ICommentService { this._onDidUpdateCommentThreads.fire(event); } - createNewCommenThread(owner: number, resource: URI, range: Range, text: string): TPromise { + createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise { const commentProvider = this._commentProviders.get(owner); if (commentProvider) { - return asWinJsPromise(token => commentProvider.createNewCommentThread(resource, range, text, token)); + return commentProvider.createNewCommentThread(resource, range, text, CancellationToken.None); } return null; } - replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): TPromise { + replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise { const commentProvider = this._commentProviders.get(owner); if (commentProvider) { - return asWinJsPromise(token => commentProvider.replyToCommentThread(resource, range, thread, text, token)); + return commentProvider.replyToCommentThread(resource, range, thread, text, CancellationToken.None); } return null; } - async getComments(resource: URI): TPromise { + getComments(resource: URI): Promise { const result = []; for (const handle of keys(this._commentProviders)) { const provider = this._commentProviders.get(handle); if ((provider).provideDocumentComments) { - result.push(asWinJsPromise(token => (provider).provideDocumentComments(resource, token))); + result.push((provider).provideDocumentComments(resource, CancellationToken.None)); } } - return TPromise.join(result); + return Promise.all(result); } } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index da24f1bb0df..eebb171da24 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -5,7 +5,6 @@ 'use strict'; import * as nls from 'vs/nls'; -import { $ } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -14,65 +13,72 @@ import * as arrays from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import * as strings from 'vs/base/common/strings'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import * as modes from 'vs/editor/common/modes'; import { peekViewBorder } from 'vs/editor/contrib/referenceSearch/referencesWidget'; import { IOptions, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { CommentGlyphWidget } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SimpleCommentEditor } from './simpleCommentEditor'; -import URI from 'vs/base/common/uri'; -import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { URI } from 'vs/base/common/uri'; +import { transparent, editorForeground, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; +import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; -const EXPAND_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-down'; -const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-up'; +const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; const COMMENT_SCHEME = 'comment'; -declare var ResizeObserver: any; export class CommentNode { private _domNode: HTMLElement; private _body: HTMLElement; private _md: HTMLElement; private _clearTimeout: any; + public get domNode(): HTMLElement { return this._domNode; } - constructor(public comment: modes.Comment) { - this._domNode = $('div.review-comment').getHTMLElement(); - this._domNode.tabIndex = 0; - let avatar = $('div.avatar-container').appendTo(this._domNode).getHTMLElement(); - let img = $('img.avatar').appendTo(avatar).getHTMLElement(); - img.src = comment.gravatar; - let commentDetailsContainer = $('.review-comment-contents').appendTo(this._domNode).getHTMLElement(); - let header = $('div').appendTo(commentDetailsContainer).getHTMLElement(); - let author = $('strong.author').appendTo(header).getHTMLElement(); + constructor( + public comment: modes.Comment, + private markdownRenderer: MarkdownRenderer, + ) { + this._domNode = dom.$('div.review-comment'); + this._domNode.tabIndex = 0; + let avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + let img = dom.append(avatar, dom.$('img.avatar')); + img.src = comment.gravatar; + let commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); + + let header = dom.append(commentDetailsContainer, dom.$('div')); + let author = dom.append(header, dom.$('strong.author')); author.innerText = comment.userName; - this._body = $('div.comment-body').appendTo(commentDetailsContainer).getHTMLElement(); - this._md = renderMarkdown(comment.body); + this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body')); + this._md = this.markdownRenderer.render(comment.body).element; this._body.appendChild(this._md); this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); this._domNode.setAttribute('role', 'treeitem'); - this._domNode.title = `${comment.userName}, ${comment.body.value}`; this._clearTimeout = null; } update(newComment: modes.Comment) { if (newComment.body !== this.comment.body) { this._body.removeChild(this._md); - this._md = renderMarkdown(newComment.body); + this._md = this.markdownRenderer.render(newComment.body).element; this._body.appendChild(this._md); } @@ -91,11 +97,10 @@ export class CommentNode { } let INMEM_MODEL_ID = 0; + export class ReviewZoneWidget extends ZoneWidget { private _headElement: HTMLElement; - protected _primaryHeading: HTMLElement; - protected _secondaryHeading: HTMLElement; - protected _metaHeading: HTMLElement; + protected _headingLabel: HTMLElement; protected _actionbarWidget: ActionBar; private _bodyElement: HTMLElement; private _commentEditor: ICodeEditor; @@ -105,13 +110,17 @@ export class ReviewZoneWidget extends ZoneWidget { private _reviewThreadReplyButton: HTMLElement; private _resizeObserver: any; private _onDidClose = new Emitter(); + private _onDidCreateThread = new Emitter(); private _isCollapsed; - private _toggleAction: Action; + private _collapseAction: Action; private _commentThread: modes.CommentThread; private _commentGlyph: CommentGlyphWidget; private _owner: number; - private _decorationIDs: string[]; private _localToDispose: IDisposable[]; + private _globalToDispose: IDisposable[]; + private _markdownRenderer: MarkdownRenderer; + private _styleElement: HTMLStyleElement; + private _error: HTMLElement; public get owner(): number { return this._owner; @@ -126,6 +135,7 @@ export class ReviewZoneWidget extends ZoneWidget { private modelService: IModelService, private themeService: IThemeService, private commentService: ICommentService, + private openerService: IOpenerService, editor: ICodeEditor, owner: number, commentThread: modes.CommentThread, @@ -136,32 +146,39 @@ export class ReviewZoneWidget extends ZoneWidget { this._owner = owner; this._commentThread = commentThread; this._isCollapsed = commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded; - this._decorationIDs = []; + this._globalToDispose = []; this._localToDispose = []; this.create(); - this.themeService.onThemeChange(this._applyTheme, this); + + this._styleElement = dom.createStyleSheet(this.domNode); + this._globalToDispose.push(this.themeService.onThemeChange(this._applyTheme, this)); + this._globalToDispose.push(this.editor.onDidChangeConfiguration(e => { + if (e.fontInfo) { + this._applyTheme(this.themeService.getTheme()); + } + })); + this._applyTheme(this.themeService.getTheme()); + + this._markdownRenderer = new MarkdownRenderer(editor, this.modeService, this.openerService); } public get onDidClose(): Event { return this._onDidClose.event; } + public get onDidCreateThread(): Event { + return this._onDidCreateThread.event; + } + protected revealLine(lineNumber: number) { // we don't do anything here as we always do the reveal ourselves. } public reveal(commentId?: string) { if (this._isCollapsed) { - if (this._decorationIDs && this._decorationIDs.length) { - let range = this.editor.getModel().getDecorationRange(this._decorationIDs[0]); - this.show(range, 2); - } else { - this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); - } + this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); } - this._bodyElement.focus(); - if (commentId) { let height = this.editor.getLayoutInfo().height; let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === commentId); @@ -170,7 +187,6 @@ export class ReviewZoneWidget extends ZoneWidget { const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode); this.editor.setScrollTop(this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top); - matchedNode[0].focus(); return; } } @@ -180,55 +196,40 @@ export class ReviewZoneWidget extends ZoneWidget { protected _fillContainer(container: HTMLElement): void { this.setCssClass('review-widget'); - this._headElement = $('.head').getHTMLElement(); + this._headElement = dom.$('.head'); container.appendChild(this._headElement); this._fillHead(this._headElement); - this._bodyElement = $('.body').getHTMLElement(); + this._bodyElement = dom.$('.body'); container.appendChild(this._bodyElement); } protected _fillHead(container: HTMLElement): void { - var titleElement = $('.review-title'). - appendTo(this._headElement). - getHTMLElement(); + var titleElement = dom.append(this._headElement, dom.$('.review-title')); - this._primaryHeading = $('span.filename').appendTo(titleElement).getHTMLElement(); - this._secondaryHeading = $('span.dirname').appendTo(titleElement).getHTMLElement(); - this._metaHeading = $('span.meta').appendTo(titleElement).getHTMLElement(); + this._headingLabel = dom.append(titleElement, dom.$('span.filename')); + this.createThreadLabel(); - let primaryHeading = 'Participants:'; - $(this._primaryHeading).safeInnerHtml(primaryHeading); - this._primaryHeading.setAttribute('aria-label', primaryHeading); - - let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); - $(this._secondaryHeading).safeInnerHtml(secondaryHeading); - this._secondaryHeading.setAttribute('aria-label', secondaryHeading); - - const actionsContainer = $('.review-actions').appendTo(this._headElement); - this._actionbarWidget = new ActionBar(actionsContainer.getHTMLElement(), {}); + const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); + this._actionbarWidget = new ActionBar(actionsContainer, {}); this._disposables.push(this._actionbarWidget); - this._toggleAction = new Action('review.expand', nls.localize('label.expand', "Expand"), this._isCollapsed ? EXPAND_ACTION_CLASS : COLLAPSE_ACTION_CLASS, true, () => { - if (this._isCollapsed) { - this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); - } - else { - if (this._commentThread.comments.length === 0) { - this.dispose(); - return null; - } - this._isCollapsed = true; - this.hide(); + this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => { + if (this._commentThread.comments.length === 0) { + this.dispose(); + return null; } + this._isCollapsed = true; + this.hide(); + return null; }); - this._actionbarWidget.push(this._toggleAction, { label: false, icon: true }); + this._actionbarWidget.push(this._collapseAction, { label: false, icon: true }); } toggleExpand() { - this._toggleAction.run(); + this._collapseAction.run(); } update(commentThread: modes.CommentThread) { @@ -255,16 +256,6 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentsElement.removeChild(commentElementsToDel[i].domNode); } - if (this._commentElements.length === 0) { - this._commentThread = commentThread; - commentThread.comments.forEach(comment => { - let newElement = new CommentNode(comment); - this._commentElements.push(newElement); - this._commentsElement.appendChild(newElement.domNode); - }); - return; - } - let lastCommentElement: HTMLElement = null; let newCommentNodeList: CommentNode[] = []; for (let i = newCommentsLen - 1; i >= 0; i--) { @@ -274,7 +265,7 @@ export class ReviewZoneWidget extends ZoneWidget { lastCommentElement = oldCommentNode[0].domNode; newCommentNodeList.unshift(oldCommentNode[0]); } else { - let newElement = new CommentNode(currentComment); + let newElement = new CommentNode(currentComment, this._markdownRenderer); newCommentNodeList.unshift(newElement); if (lastCommentElement) { this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); @@ -288,58 +279,36 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentThread = commentThread; this._commentElements = newCommentNodeList; - let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); - $(this._secondaryHeading).safeInnerHtml(secondaryHeading); + this.createThreadLabel(); } protected _doLayout(heightInPixel: number, widthInPixel: number): void { - this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 20 /* margin */ }); + this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); } - display(lineNumber: number) { - this._commentGlyph = new CommentGlyphWidget(`review_${lineNumber}`, this.editor, lineNumber, false, () => { + display(lineNumber: number, commentsOptions: ModelDecorationOptions) { + this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber, commentsOptions, () => { this.toggleExpand(); }); - this.editor.layoutContentWidget(this._commentGlyph); this._localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this._localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); - this._localToDispose.push(this.editor.onDidChangeModelContent((e) => { - // If the widget has been opened, the position is set and can be relied on for updating the glyph position - if (this.position) { - if (this.position.lineNumber !== this._commentGlyph.getPosition().position.lineNumber) { - this._commentGlyph.setLineNumber(this.position.lineNumber); - this.editor.layoutContentWidget(this._commentGlyph); - } - } else { - // Otherwise manually calculate position change :( - const positionChange = e.changes.map(change => { - if (change.range.startLineNumber < change.range.endLineNumber) { - return change.range.startLineNumber - change.range.endLineNumber; - } else { - return change.text.split(e.eol).length - 1; - } - }).reduce((prev, curr) => prev + curr, 0); - this._commentGlyph.setLineNumber(this._commentGlyph.getPosition().position.lineNumber + positionChange); - this.editor.layoutContentWidget(this._commentGlyph); - } - })); var headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2); this._headElement.style.height = `${headHeight}px`; this._headElement.style.lineHeight = this._headElement.style.height; - this._commentsElement = $('div.comments-container').appendTo(this._bodyElement).getHTMLElement(); + this._commentsElement = dom.append(this._bodyElement, dom.$('div.comments-container')); this._commentsElement.setAttribute('role', 'presentation'); this._commentElements = []; for (let i = 0; i < this._commentThread.comments.length; i++) { - let newCommentNode = new CommentNode(this._commentThread.comments[i]); + let newCommentNode = new CommentNode(this._commentThread.comments[i], this._markdownRenderer); this._commentElements.push(newCommentNode); this._commentsElement.appendChild(newCommentNode.domNode); } const hasExistingComments = this._commentThread.comments.length > 0; - this._commentForm = $('.comment-form').appendTo(this._bodyElement).getHTMLElement(); + this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions()); const modeId = hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID; const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md`); @@ -351,66 +320,66 @@ export class ReviewZoneWidget extends ZoneWidget { this.setCommentEditorDecorations(); // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - this.createCommentButton(); + if (hasExistingComments) { + this.createReplyButton(); + } else { + if (!dom.hasClass(this._commentForm, 'expand')) { + dom.addClass(this._commentForm, 'expand'); + this._commentEditor.focus(); + } + } + this._localToDispose.push(this._commentEditor.onKeyDown((ev: IKeyboardEvent) => { const hasExistingComments = this._commentThread.comments.length > 0; - if (this._commentEditor.getModel().getValueLength() === 0 && ev.keyCode === KeyCode.Escape && hasExistingComments) { - if (dom.hasClass(this._commentForm, 'expand')) { - dom.removeClass(this._commentForm, 'expand'); + + if (this._commentEditor.getModel().getValueLength() === 0 && ev.keyCode === KeyCode.Escape) { + if (hasExistingComments) { + if (dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + } else { + this.dispose(); } } + + if (this._commentEditor.getModel().getValueLength() !== 0 && ev.keyCode === KeyCode.Enter && (ev.ctrlKey || ev.metaKey)) { + let lineNumber = this._commentGlyph.getPosition().position.lineNumber; + this.createComment(lineNumber); + } })); - const formActions = $('.form-actions').appendTo(this._commentForm).getHTMLElement(); + this._error = dom.append(this._commentForm, dom.$('.validation-error.hidden')); + + const formActions = dom.append(this._commentForm, dom.$('.form-actions')); const button = new Button(formActions); attachButtonStyler(button, this.themeService); button.label = 'Add comment'; - button.onDidClick(async () => { - let newCommentThread; - if (this._commentThread.threadId) { - // reply - newCommentThread = await this.commentService.replyToCommentThread( - this._owner, - this.editor.getModel().uri, - new Range(lineNumber, 1, lineNumber, 1), - this._commentThread, - this._commentEditor.getValue() - ); + + button.enabled = false; + this._localToDispose.push(this._commentEditor.onDidChangeModelContent(_ => { + if (this._commentEditor.getValue()) { + button.enabled = true; } else { - newCommentThread = await this.commentService.createNewCommenThread( - this._owner, - this.editor.getModel().uri, - new Range(lineNumber, 1, lineNumber, 1), - this._commentEditor.getValue() - ); + button.enabled = false; } + })); - this._commentEditor.setValue(''); - if (dom.hasClass(this._commentForm, 'expand')) { - dom.removeClass(this._commentForm, 'expand'); - } - - if (newCommentThread) { - this.update(newCommentThread); - } + button.onDidClick(async () => { + let lineNumber = this._commentGlyph.getPosition().position.lineNumber; + this.createComment(lineNumber); }); + this._resizeObserver = new MutationObserver(this._refresh.bind(this)); - this._resizeObserver = new ResizeObserver(entries => { - if (entries[0].target === this._bodyElement && !this._isCollapsed) { - const lineHeight = this.editor.getConfiguration().lineHeight; - const arrowHeight = Math.round(lineHeight / 3); - const frameThickness = Math.round(lineHeight / 9) * 2; - - const computedLinesNumber = Math.ceil((headHeight + entries[0].contentRect.height + arrowHeight + frameThickness) / lineHeight); - this._relayout(computedLinesNumber); - } + this._resizeObserver.observe(this._bodyElement, { + attributes: true, + childList: true, + characterData: true, + subtree: true }); - this._resizeObserver.observe(this._bodyElement); - if (this._commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded) { this.show({ lineNumber: lineNumber, column: 1 }, 2); } @@ -421,108 +390,196 @@ export class ReviewZoneWidget extends ZoneWidget { } } - createCommentButton() { - const hasExistingComments = this._commentThread.comments.length > 0; - if (hasExistingComments) { - this._reviewThreadReplyButton = $('button.review-thread-reply-button').appendTo(this._commentForm).getHTMLElement(); - this._reviewThreadReplyButton.title = 'Reply...'; - this._reviewThreadReplyButton.textContent = 'Reply...'; - // bind click/escape actions for reviewThreadReplyButton and textArea - this._reviewThreadReplyButton.onclick = () => { - if (!dom.hasClass(this._commentForm, 'expand')) { - dom.addClass(this._commentForm, 'expand'); - this._commentEditor.focus(); - } - }; + private async createComment(lineNumber: number): Promise { + try { + let newCommentThread; + const isReply = this._commentThread.threadId !== null; - this._commentEditor.onDidBlurEditorWidget(() => { - if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) { + if (isReply) { + newCommentThread = await this.commentService.replyToCommentThread( + this._owner, + this.editor.getModel().uri, + new Range(lineNumber, 1, lineNumber, 1), + this._commentThread, + this._commentEditor.getValue() + ); + } else { + newCommentThread = await this.commentService.createNewCommentThread( + this._owner, + this.editor.getModel().uri, + new Range(lineNumber, 1, lineNumber, 1), + this._commentEditor.getValue() + ); + + if (newCommentThread) { + this.createReplyButton(); + } + } + + if (newCommentThread) { + this._commentEditor.setValue(''); + if (dom.hasClass(this._commentForm, 'expand')) { dom.removeClass(this._commentForm, 'expand'); } - }); + this._commentEditor.getDomNode().style.outline = ''; + this._error.textContent = ''; + dom.addClass(this._error, 'hidden'); + this.update(newCommentThread); + + if (!isReply) { + this._onDidCreateThread.fire(this); + } + } + } catch (e) { + this._error.textContent = e.message + ? nls.localize('commentCreationError', "Adding a comment failed: {0}.", e.message) + : nls.localize('commentCreationDefaultError', "Adding a comment failed. Please try again or report an issue with the extension if the problem persists."); + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + dom.removeClass(this._error, 'hidden'); + } + } + + private createThreadLabel() { + let label: string; + if (this._commentThread.comments.length) { + const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); + label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList); } else { - dom.addClass(this._commentForm, 'expand'); + label = nls.localize('startThread', "Start discussion"); + } + + this._headingLabel.innerHTML = strings.escape(label); + this._headingLabel.setAttribute('aria-label', label); + } + + private createReplyButton() { + this._reviewThreadReplyButton = dom.append(this._commentForm, dom.$('button.review-thread-reply-button')); + this._reviewThreadReplyButton.title = nls.localize('reply', "Reply..."); + this._reviewThreadReplyButton.textContent = nls.localize('reply', "Reply..."); + // bind click/escape actions for reviewThreadReplyButton and textArea + this._reviewThreadReplyButton.onclick = () => { + if (!dom.hasClass(this._commentForm, 'expand')) { + dom.addClass(this._commentForm, 'expand'); + this._commentEditor.focus(); + } + }; + + this._commentEditor.onDidBlurEditorWidget(() => { + if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + }); + } + + _refresh() { + if (!this._isCollapsed && this._bodyElement) { + let dimensions = dom.getClientArea(this._bodyElement); + const headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2); + const lineHeight = this.editor.getConfiguration().lineHeight; + const arrowHeight = Math.round(lineHeight / 3); + const frameThickness = Math.round(lineHeight / 9) * 2; + + const computedLinesNumber = Math.ceil((headHeight + dimensions.height + arrowHeight + frameThickness) / lineHeight); + this._relayout(computedLinesNumber); } } private setCommentEditorDecorations() { - let model = this._commentEditor.getModel(); - let valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments.length > 0; - let placeholder = valueLength > 0 ? '' : (hasExistingComments ? 'Reply...' : 'Type a new comment'); - const decorations = [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: placeholder, - color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString() + const model = this._commentEditor && this._commentEditor.getModel(); + if (model) { + let valueLength = model.getValueLength(); + const hasExistingComments = this._commentThread.comments.length > 0; + let keybinding = platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'; + let placeholder = valueLength > 0 + ? '' + : (hasExistingComments + ? `Reply... (press ${keybinding} to submit)` + : `Type a new comment (press ${keybinding} to submit)`); + const decorations = [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: placeholder, + color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString() + } } - } - }]; + }]; - this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations); + } } - private mouseDownInfo: { lineNumber: number, iconClicked: boolean }; + private mouseDownInfo: { lineNumber: number }; private onEditorMouseDown(e: IEditorMouseEvent): void { - if (!e.event.leftButton) { - return; - } + this.mouseDownInfo = null; + + const range = e.target.range; - let range = e.target.range; if (!range) { return; } - let iconClicked = false; - switch (e.target.type) { - case MouseTargetType.GUTTER_GLYPH_MARGIN: - iconClicked = true; - break; - default: - return; + if (!e.event.leftButton) { + return; } - this.mouseDownInfo = { lineNumber: range.startLineNumber, iconClicked }; + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + return; + } + + const data = e.target.detail as IMarginData; + const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; + + // don't collide with folding and git decorations + if (gutterOffsetX > 14) { + return; + } + + this.mouseDownInfo = { lineNumber: range.startLineNumber }; } private onEditorMouseUp(e: IEditorMouseEvent): void { if (!this.mouseDownInfo) { return; } - let lineNumber = this.mouseDownInfo.lineNumber; - let iconClicked = this.mouseDownInfo.iconClicked; - let range = e.target.range; + const { lineNumber } = this.mouseDownInfo; + this.mouseDownInfo = null; + + const range = e.target.range; + if (!range || range.startLineNumber !== lineNumber) { return; } - if (this.position && this.position.lineNumber !== lineNumber) { + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { return; } - if (!this.position && lineNumber !== this._commentThread.range.startLineNumber) { + if (!e.target.element) { return; } - if (iconClicked) { - if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { - return; + if (this._commentGlyph && this._commentGlyph.getPosition().position.lineNumber !== lineNumber) { + return; + } + + if (e.target.element.className.indexOf('comment-thread') >= 0) { + if (this._isCollapsed) { + this.show({ lineNumber: lineNumber, column: 1 }, 2); + } else { + this.hide(); + if (this._commentThread === null || this._commentThread.threadId === null) { + this.dispose(); + } } } - - if (this._isCollapsed) { - this.show({ lineNumber: lineNumber, column: 1 }, 2); - } else { - this.hide(); - } } private _applyTheme(theme: ITheme) { @@ -531,11 +588,72 @@ export class ReviewZoneWidget extends ZoneWidget { arrowColor: borderColor, frameColor: borderColor }); + + const content: string[] = []; + const linkColor = theme.getColor(textLinkForeground); + if (linkColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a { color: ${linkColor} }`); + } + + const linkActiveColor = theme.getColor(textLinkActiveForeground); + if (linkActiveColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a:hover, a:active { color: ${linkActiveColor} }`); + } + + const focusColor = theme.getColor(focusBorder); + if (focusColor) { + content.push(`.monaco-editor .review-widget .body .review-comment a:focus { outline: 1px solid ${focusColor}; }`); + content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); + } + + const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); + if (blockQuoteBackground) { + content.push(`.monaco-editor .review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`); + } + + const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder); + if (blockQuoteBOrder) { + content.push(`.monaco-editor .review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`); + } + + const hcBorder = theme.getColor(contrastBorder); + if (hcBorder) { + content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); + content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor { outline: 1px solid ${hcBorder}; }`); + } + + const errorBorder = theme.getColor(inputValidationErrorBorder); + if (errorBorder) { + content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { border: 1px solid ${errorBorder}; }`); + } + + const errorBackground = theme.getColor(inputValidationErrorBackground); + if (errorBackground) { + content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { background: ${errorBackground}; }`); + } + + const errorForeground = theme.getColor(inputValidationErrorForeground); + if (errorForeground) { + content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`); + } + + const fontInfo = this.editor.getConfiguration().fontInfo; + content.push(`.monaco-editor .review-widget .body code { + font-family: ${fontInfo.fontFamily}; + font-size: ${fontInfo.fontSize}px; + font-weight: ${fontInfo.fontWeight}; + }`); + + this._styleElement.innerHTML = content.join('\n'); + + // Editor decorations should also be responsive to theme changes + this.setCommentEditorDecorations(); } show(rangeOrPos: IRange | IPosition, heightInLines: number): void { this._isCollapsed = false; super.show(rangeOrPos, heightInLines); + this._refresh(); } hide() { @@ -549,13 +667,13 @@ export class ReviewZoneWidget extends ZoneWidget { this._resizeObserver.disconnect(); this._resizeObserver = null; } - this.editor.changeDecorations(accessor => { - accessor.deltaDecorations(this._decorationIDs, []); - }); + if (this._commentGlyph) { - this.editor.removeContentWidget(this._commentGlyph); + this._commentGlyph.dispose(); this._commentGlyph = null; } + + this._globalToDispose.forEach(global => global.dispose()); this._localToDispose.forEach(local => local.dispose()); this._onDidClose.fire(); } diff --git a/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts b/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts index 751fd1a1371..80bdf78ac19 100644 --- a/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts @@ -36,6 +36,6 @@ Registry.as(PanelExtensions.Panels).registerPanel(new PanelDescri )); // Register view location updater -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommentPanelVisibilityUpdater, LifecyclePhase.Running); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommentPanelVisibilityUpdater, LifecyclePhase.Restoring); registerSingleton(ICommentService, CommentService); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index a61caf0eaf1..b1b4b18fff5 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -5,29 +5,35 @@ 'use strict'; import 'vs/css!./media/review'; -import { $ } from 'vs/base/browser/builder'; +import * as nls from 'vs/nls'; +import { $ } from 'vs/base/browser/dom'; +import { findFirstInSorted } from 'vs/base/common/arrays'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ICodeEditor, IEditorMouseEvent, IViewZone } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditor, IEditorMouseEvent, IViewZone, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution, EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IRange } from 'vs/editor/common/core/range'; import * as modes from 'vs/editor/common/modes'; -import { peekViewEditorBackground, peekViewResultsBackground, peekViewResultsSelectionBackground } from 'vs/editor/contrib/referenceSearch/referencesWidget'; +import { peekViewResultsBackground, peekViewResultsSelectionBackground, peekViewTitleBackground } from 'vs/editor/contrib/referenceSearch/referencesWidget'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { editorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { CommentThreadCollapsibleState } from 'vs/workbench/api/node/extHostTypes'; -import { ReviewModel } from 'vs/workbench/parts/comments/common/reviewModel'; -import { CommentGlyphWidget } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget'; import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/parts/comments/electron-browser/commentThreadWidget'; import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelDecorationOptions } from 'vs/editor/common/model'; +import { Color, RGBA } from 'vs/base/common/color'; +import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); @@ -42,7 +48,7 @@ export class ReviewViewZone implements IViewZone { this.afterLineNumber = afterLineNumber; this.callback = onDomNodeTop; - this.domNode = $('.review-viewzone').getHTMLElement(); + this.domNode = $('.review-viewzone'); } onDomNodeTop(top: number): void { @@ -50,6 +56,110 @@ export class ReviewViewZone implements IViewZone { } } +const overviewRulerDefault = new Color(new RGBA(197, 197, 197, 1)); + +export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: overviewRulerDefault, light: overviewRulerDefault, hc: overviewRulerDefault }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges.')); + +class CommentingRangeDecoration { + private _decorationId: string; + + get id(): string { + return this._decorationId; + } + + constructor(private _editor: ICodeEditor, private _ownerId: number, private _range: IRange, private _reply: modes.Command, commentingOptions: ModelDecorationOptions) { + const startLineNumber = _range.startLineNumber; + const endLineNumber = _range.endLineNumber; + let commentingRangeDecorations = [{ + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: commentingOptions + }]; + + let model = this._editor.getModel(); + if (model) { + this._decorationId = model.deltaDecorations([this._decorationId], commentingRangeDecorations)[0]; + } + } + + getCommentAction(): { replyCommand: modes.Command, ownerId: number } { + return { + replyCommand: this._reply, + ownerId: this._ownerId + }; + } + + getOriginalRange() { + return this._range; + } + + getActiveRange() { + return this._editor.getModel().getDecorationRange(this._decorationId); + } +} +class CommentingRangeDecorator { + + static createDecoration(className: string, foregroundColor: string, options: { gutter: boolean, overview: boolean }): ModelDecorationOptions { + const decorationOptions: IModelDecorationOptions = { + isWholeLine: true, + }; + + decorationOptions.linesDecorationsClassName = `comment-range-glyph ${className}`; + return ModelDecorationOptions.createDynamic(decorationOptions); + } + + private commentingOptions: ModelDecorationOptions; + public commentsOptions: ModelDecorationOptions; + private commentingRangeDecorations: CommentingRangeDecoration[] = []; + private disposables: IDisposable[] = []; + + constructor( + ) { + const options = { gutter: true, overview: false }; + this.commentingOptions = CommentingRangeDecorator.createDecoration('comment-diff-added', overviewRulerCommentingRangeForeground, options); + this.commentsOptions = CommentingRangeDecorator.createDecoration('comment-thread', overviewRulerCommentingRangeForeground, options); + } + + update(editor: ICodeEditor, commentInfos: modes.CommentInfo[]) { + let model = editor.getModel(); + if (!model) { + return; + } + + let commentingRangeDecorations = []; + for (let i = 0; i < commentInfos.length; i++) { + let info = commentInfos[i]; + info.commentingRanges.forEach(range => { + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, range, info.reply, this.commentingOptions)); + }); + } + + let oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id); + editor.deltaDecorations(oldDecorations, []); + + this.commentingRangeDecorations = commentingRangeDecorations; + } + + getMatchedCommentAction(line: number) { + for (let i = 0; i < this.commentingRangeDecorations.length; i++) { + let range = this.commentingRangeDecorations[i].getActiveRange(); + + if (range.startLineNumber <= line && line <= range.endLineNumber) { + return this.commentingRangeDecorations[i].getCommentAction(); + } + } + + return null; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + this.commentingRangeDecorations = []; + } +} + export class ReviewController implements IEditorContribution { private globalToDispose: IDisposable[]; private localToDispose: IDisposable[]; @@ -58,9 +168,11 @@ export class ReviewController implements IEditorContribution { private _commentWidgets: ReviewZoneWidget[]; private _reviewPanelVisible: IContextKey; private _commentInfos: modes.CommentInfo[]; - private _reviewModel: ReviewModel; - private _newCommentGlyph: CommentGlyphWidget; - private _hasSetComments: boolean; + // private _hasSetComments: boolean; + private _commentingRangeDecorator: CommentingRangeDecorator; + private mouseDownInfo: { lineNumber: number } | null = null; + private _commentingRangeSpaceReserved = false; + constructor( editor: ICodeEditor, @@ -72,7 +184,7 @@ export class ReviewController implements IEditorContribution { @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, @ICodeEditorService private codeEditorService: ICodeEditorService, - + @IOpenerService private openerService: IOpenerService ) { this.editor = editor; this.globalToDispose = []; @@ -80,30 +192,10 @@ export class ReviewController implements IEditorContribution { this._commentInfos = []; this._commentWidgets = []; this._newCommentWidget = null; - this._newCommentGlyph = null; - this._hasSetComments = false; + // this._hasSetComments = false; this._reviewPanelVisible = ctxReviewPanelVisible.bindTo(contextKeyService); - this._reviewModel = new ReviewModel(); - - this._reviewModel.onDidChangeStyle(style => { - if (this._newCommentWidget) { - this._newCommentWidget.dispose(); - this._newCommentWidget = null; - } - - this._commentWidgets.forEach(zone => { - zone.dispose(); - }); - - this._commentInfos.forEach(info => { - info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {}); - zoneWidget.display(thread.range.startLineNumber); - this._commentWidgets.push(zoneWidget); - }); - }); - }); + this._commentingRangeDecorator = new CommentingRangeDecorator(); this.globalToDispose.push(this.commentService.onDidDeleteDataProvider(e => { // Remove new comment widget and glyph, refresh comments @@ -112,11 +204,6 @@ export class ReviewController implements IEditorContribution { this._newCommentWidget = null; } - if (this._newCommentGlyph) { - this.editor.removeContentWidget(this._newCommentGlyph); - this._newCommentGlyph = null; - } - this.getComments(); })); @@ -154,6 +241,56 @@ export class ReviewController implements IEditorContribution { } } + public nextCommentThread(): void { + if (!this._commentWidgets.length) { + return; + } + + const after = this.editor.getSelection().getEndPosition(); + const sortedWidgets = this._commentWidgets.sort((a, b) => { + if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) { + return -1; + } + + if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) { + return 1; + } + + if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) { + return -1; + } + + if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) { + return 1; + } + + return 0; + }); + + let idx = findFirstInSorted(sortedWidgets, widget => { + if (widget.commentThread.range.startLineNumber > after.lineNumber) { + return true; + } + + if (widget.commentThread.range.startLineNumber < after.lineNumber) { + return false; + } + + if (widget.commentThread.range.startColumn > after.column) { + return true; + } + return false; + }); + + if (idx === this._commentWidgets.length) { + this._commentWidgets[0].reveal(); + this.editor.setSelection(this._commentWidgets[0].commentThread.range); + } else { + sortedWidgets[idx].reveal(); + this.editor.setSelection(sortedWidgets[idx].commentThread.range); + } + } + getId(): string { return ID; } @@ -174,33 +311,30 @@ export class ReviewController implements IEditorContribution { public onModelChanged(): void { this.localToDispose = dispose(this.localToDispose); if (this._newCommentWidget) { - // todo store view state. + // todo@peng store view state. this._newCommentWidget.dispose(); this._newCommentWidget = null; } - if (this._newCommentGlyph) { - this.editor.removeContentWidget(this._newCommentGlyph); - this._newCommentGlyph = null; - } - this._commentWidgets.forEach(zone => { zone.dispose(); }); this._commentWidgets = []; - this.localToDispose.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e))); + this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); + this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); this.localToDispose.push(this.editor.onDidChangeModelContent(() => { - if (this._newCommentGlyph) { - this.editor.removeContentWidget(this._newCommentGlyph); - this._newCommentGlyph = null; - } })); this.localToDispose.push(this.commentService.onDidUpdateCommentThreads(e => { const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri; if (!editorURI) { return; } + + if (!this._commentInfos.some(info => info.owner === e.owner)) { + return; + } + let added = e.added.filter(thread => thread.resource.toString() === editorURI.toString()); let removed = e.removed.filter(thread => thread.resource.toString() === editorURI.toString()); let changed = e.changed.filter(thread => thread.resource.toString() === editorURI.toString()); @@ -222,8 +356,8 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, e.owner, thread, {}); - zoneWidget.display(thread.range.startLineNumber); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, e.owner, thread, {}); + zoneWidget.display(thread.range.startLineNumber, this._commentingRangeDecorator.commentsOptions); this._commentWidgets.push(zoneWidget); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); }); @@ -231,7 +365,12 @@ export class ReviewController implements IEditorContribution { } private addComment(lineNumber: number) { - let newCommentInfo = this.getNewCommentAction(lineNumber); + if (this._newCommentWidget !== null) { + this.notificationService.warn(`Please submit the comment at line ${this._newCommentWidget.position.lineNumber} before creating a new one.`); + return; + } + + let newCommentInfo = this._commentingRangeDecorator.getMatchedCommentAction(lineNumber); if (!newCommentInfo) { return; } @@ -239,7 +378,7 @@ export class ReviewController implements IEditorContribution { // add new comment this._reviewPanelVisible.set(true); const { replyCommand, ownerId } = newCommentInfo; - this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, ownerId, { + this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, ownerId, { threadId: null, resource: null, comments: [], @@ -253,94 +392,131 @@ export class ReviewController implements IEditorContribution { collapsibleState: CommentThreadCollapsibleState.Expanded, }, {}); - this._newCommentWidget.onDidClose(e => { + this.localToDispose.push(this._newCommentWidget.onDidClose(e => { this._newCommentWidget = null; - }); - this._newCommentWidget.display(lineNumber); + })); + + this.localToDispose.push(this._newCommentWidget.onDidCreateThread(commentWidget => { + const thread = commentWidget.commentThread; + this._commentWidgets.push(commentWidget); + this._commentInfos.filter(info => info.owner === commentWidget.owner)[0].threads.push(thread); + this._newCommentWidget = null; + })); + + this._newCommentWidget.display(lineNumber, this._commentingRangeDecorator.commentsOptions); } - private onEditorMouseMove(e: IEditorMouseEvent): void { - if (!this._hasSetComments) { + private onEditorMouseDown(e: IEditorMouseEvent): void { + this.mouseDownInfo = null; + + const range = e.target.range; + + if (!range) { return; } - const hasCommentingRanges = this._commentInfos.length && this._commentInfos.some(info => !!info.commentingRanges.length); - if (hasCommentingRanges && e.target.position && e.target.position.lineNumber !== undefined) { - if (this._newCommentGlyph && e.target.element.className !== 'comment-hint') { - this.editor.removeContentWidget(this._newCommentGlyph); - } + if (!e.event.leftButton) { + return; + } + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + return; + } + + const data = e.target.detail as IMarginData; + const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; + + // don't collide with folding and git decorations + if (gutterOffsetX > 14) { + return; + } + + this.mouseDownInfo = { lineNumber: range.startLineNumber }; + } + + private onEditorMouseUp(e: IEditorMouseEvent): void { + if (!this.mouseDownInfo) { + return; + } + + const { lineNumber } = this.mouseDownInfo; + this.mouseDownInfo = null; + + const range = e.target.range; + + if (!range || range.startLineNumber !== lineNumber) { + return; + } + + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + return; + } + + if (!e.target.element) { + return; + } + + if (e.target.element.className.indexOf('comment-diff-added') >= 0) { const lineNumber = e.target.position.lineNumber; - if (!this.isExistingCommentThreadAtLine(lineNumber)) { - this._newCommentGlyph = this.isLineInCommentingRange(lineNumber) - ? this._newCommentGlyph = new CommentGlyphWidget('comment-hint', this.editor, lineNumber, false, () => { - this.addComment(lineNumber); - }) - : this._newCommentGlyph = new CommentGlyphWidget('comment-hint', this.editor, lineNumber, true, () => { - this.notificationService.warn('Commenting is not supported outside of diff hunk areas.'); - }); - - this.editor.layoutContentWidget(this._newCommentGlyph); - } + this.addComment(lineNumber); } } - private getNewCommentAction(line: number): { replyCommand: modes.Command, ownerId: number } { - for (let i = 0; i < this._commentInfos.length; i++) { - const commentInfo = this._commentInfos[i]; - const lineWithinRange = commentInfo.commentingRanges.some(range => - range.startLineNumber <= line && line <= range.endLineNumber - ); - - if (lineWithinRange) { - return { - replyCommand: commentInfo.reply, - ownerId: commentInfo.owner - }; - } - } - - return null; - } - - private isLineInCommentingRange(line: number): boolean { - return this._commentInfos.some(commentInfo => { - return commentInfo.commentingRanges.some(range => - range.startLineNumber <= line && line <= range.endLineNumber - ); - }); - } - - private isExistingCommentThreadAtLine(line: number): boolean { - const existingThread = this._commentInfos.some(commentInfo => { - return commentInfo.threads.some(thread => - thread.range.startLineNumber === line - ); - }); - - const existingNewComment = this._newCommentWidget && this._newCommentWidget.position && this._newCommentWidget.position.lineNumber === line; - - return existingThread || existingNewComment; - } - setComments(commentInfos: modes.CommentInfo[]): void { this._commentInfos = commentInfos; - this._hasSetComments = true; + let lineDecorationsWidth: number = this.editor.getConfiguration().layoutInfo.decorationsWidth; + + if (this._commentInfos.some(info => Boolean(info.commentingRanges && info.commentingRanges.length))) { + if (!this._commentingRangeSpaceReserved) { + this._commentingRangeSpaceReserved = true; + let extraEditorClassName = []; + if (this.editor.getRawConfiguration().extraEditorClassName) { + extraEditorClassName = this.editor.getRawConfiguration().extraEditorClassName.split(' '); + } + + if (this.editor.getConfiguration().contribInfo.folding) { + lineDecorationsWidth -= 16; + } + lineDecorationsWidth += 9; + extraEditorClassName.push('inline-comment'); + this.editor.updateOptions({ + extraEditorClassName: extraEditorClassName.join(' '), + lineDecorationsWidth: lineDecorationsWidth + }); + + // we only update the lineDecorationsWidth property but keep the width of the whole editor. + const originalLayoutInfo = this.editor.getLayoutInfo(); + + this.editor.layout({ + width: originalLayoutInfo.width, + height: originalLayoutInfo.height + }); + } + } + + // this._hasSetComments = true; // create viewzones this._commentWidgets.forEach(zone => { zone.dispose(); }); + this._commentWidgets = []; + this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.editor, info.owner, thread, {}); - zoneWidget.display(thread.range.startLineNumber); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, info.owner, thread, {}); + zoneWidget.display(thread.range.startLineNumber, this._commentingRangeDecorator.commentsOptions); this._commentWidgets.push(zoneWidget); }); }); - } + const commentingRanges = []; + this._commentInfos.forEach(info => { + commentingRanges.push(...info.commentingRanges); + }); + this._commentingRangeDecorator.update(this.editor, this._commentInfos); + } public closeWidget(): void { this._reviewPanelVisible.reset(); @@ -359,12 +535,31 @@ export class ReviewController implements IEditorContribution { } } -registerEditorContribution(ReviewController); +export class NextCommentThreadAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.nextCommentThreadAction', + label: nls.localize('nextCommentThreadAction', "Go to Next Comment Thread"), + alias: 'Go to Next Comment Thread', + precondition: null, + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = ReviewController.get(editor); + if (controller) { + controller.nextCommentThread(); + } + } +} + +registerEditorContribution(ReviewController); +registerEditorAction(NextCommentThreadAction); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeReviewPanel', - weight: KeybindingsRegistry.WEIGHT.editorContrib(), + weight: KeybindingWeight.EditorContrib, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ctxReviewPanelVisible, @@ -405,7 +600,7 @@ registerThemingParticipant((theme, collector) => { `}`); } - let monacoEditorBackground = theme.getColor(peekViewEditorBackground); + let monacoEditorBackground = theme.getColor(peekViewTitleBackground); if (monacoEditorBackground) { collector.addRule( `.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` + @@ -439,4 +634,22 @@ registerThemingParticipant((theme, collector) => { `}` ); } + + const commentingRangeForeground = theme.getColor(overviewRulerCommentingRangeForeground); + if (commentingRangeForeground) { + collector.addRule(` + .monaco-editor .comment-diff-added { + border-left: 3px solid ${commentingRangeForeground}; + } + .monaco-editor .comment-diff-added:before { + background: ${commentingRangeForeground}; + } + .monaco-editor .comment-thread { + border-left: 3px solid ${commentingRangeForeground}; + } + .monaco-editor .comment-thread:before { + background: ${commentingRangeForeground}; + } + `); + } }); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts index d75c7d5aa5d..7a890b78107 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts @@ -10,7 +10,7 @@ import { debounceEvent } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { CollapseAllAction, DefaultAccessibilityProvider, DefaultController, DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults'; import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/modes'; +import { CommentThreadChangedEvent } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -19,9 +19,11 @@ import { Panel } from 'vs/workbench/browser/panel'; import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/parts/comments/common/commentModel'; import { ReviewController } from 'vs/workbench/parts/comments/electron-browser/commentsEditorContribution'; import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/parts/comments/electron-browser/commentsTreeViewer'; -import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; +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 { IOpenerService } from 'vs/platform/opener/common/opener'; export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; export const COMMENTS_PANEL_TITLE = 'Comments'; @@ -39,6 +41,7 @@ export class CommentsPanel extends Panel { @ICommentService private commentService: ICommentService, @IEditorService private editorService: IEditorService, @ICommandService private commandService: ICommandService, + @IOpenerService private openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService ) { @@ -60,9 +63,37 @@ export class CommentsPanel extends Panel { this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this); this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this); + const styleElement = dom.createStyleSheet(parent); + this.applyStyles(styleElement); + this.themeService.onThemeChange(_ => { + this.applyStyles(styleElement); + }); + return this.render(); } + private applyStyles(styleElement: HTMLStyleElement) { + const content: string[] = []; + + const theme = this.themeService.getTheme(); + const linkColor = theme.getColor(textLinkForeground); + if (linkColor) { + content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`); + } + + const linkActiveColor = theme.getColor(textLinkActiveForeground); + if (linkActiveColor) { + content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`); + } + + const focusColor = theme.getColor(focusBorder); + if (focusColor) { + content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); + } + + styleElement.innerHTML = content.join('\n'); + } + private render(): TPromise { dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads()); return this.tree.setInput(this.commentsModel).then(() => { @@ -73,7 +104,7 @@ export class CommentsPanel extends Panel { public getActions(): IAction[] { if (!this.collapseAllAction) { this.collapseAllAction = this.instantiationService.createInstance(CollapseAllAction, this.tree, this.commentsModel.hasCommentThreads()); - this.toUnbind.push(this.collapseAllAction); + this._register(this.collapseAllAction); } return [this.collapseAllAction]; @@ -101,7 +132,7 @@ export class CommentsPanel extends Panel { private createTree(): void { this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { dataSource: new CommentsDataSource(), - renderer: new CommentsModelRenderer(this.instantiationService), + renderer: new CommentsModelRenderer(this.instantiationService, this.openerService), accessibilityProvider: new DefaultAccessibilityProvider, controller: new DefaultController(), dnd: new DefaultDragAndDrop(), @@ -201,7 +232,6 @@ export class CommentsPanel extends Panel { const control = editor.getControl(); if (threadToReveal && isCodeEditor(control)) { const controller = ReviewController.get(control); - console.log(commentToReveal.command); controller.revealCommentThread(threadToReveal, commentToReveal.commentId); } setCommentsForFile = null; @@ -212,6 +242,18 @@ export class CommentsPanel extends Panel { return true; } + public setVisible(visible: boolean): TPromise { + const wasVisible = this.isVisible(); + return super.setVisible(visible) + .then(() => { + if (this.isVisible()) { + if (!wasVisible) { + this.refresh(); + } + } + }); + } + private refresh(): void { if (this.isVisible()) { this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads(); @@ -225,13 +267,15 @@ export class CommentsPanel extends Panel { } } - private onAllCommentsChanged(e: CommentThread[]): void { - this.commentsModel.setCommentThreads(e); + private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void { + this.commentsModel.setCommentThreads(e.ownerId, e.commentThreads); this.refresh(); } private onCommentsUpdated(e: CommentThreadChangedEvent): void { - this.commentsModel.updateCommentThreads(e); - this.refresh(); + const didUpdate = this.commentsModel.updateCommentThreads(e); + if (didUpdate) { + this.refresh(); + } } } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts b/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts index df16d651425..4ead88c0914 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsTreeViewer.ts @@ -4,10 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { IDataSource, IFilter, IRenderer as ITreeRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/parts/comments/common/commentModel'; @@ -59,6 +64,7 @@ interface ICommentThreadTemplateData { icon: HTMLImageElement; userName: HTMLSpanElement; commentText: HTMLElement; + disposables: Disposable[]; } export class CommentsModelRenderer implements ITreeRenderer { @@ -67,7 +73,8 @@ export class CommentsModelRenderer implements ITreeRenderer { constructor( - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService ) { } @@ -99,6 +106,10 @@ export class CommentsModelRenderer implements ITreeRenderer { switch (templateId) { case CommentsModelRenderer.RESOURCE_ID: (templateData).resourceLabel.dispose(); + break; + case CommentsModelRenderer.COMMENT_ID: + (templateData).disposables.forEach(disposeable => disposeable.dispose()); + break; } } @@ -124,6 +135,7 @@ export class CommentsModelRenderer implements ITreeRenderer { const labelContainer = dom.append(container, dom.$('.comment-container')); data.userName = dom.append(labelContainer, dom.$('.user')); data.commentText = dom.append(labelContainer, dom.$('.text')); + data.disposables = []; return data; } @@ -134,7 +146,26 @@ export class CommentsModelRenderer implements ITreeRenderer { private renderCommentElement(tree: ITree, element: CommentNode, templateData: ICommentThreadTemplateData) { templateData.userName.textContent = element.comment.userName; - templateData.commentText.innerHTML = renderMarkdown(element.comment.body, { inline: true }).innerHTML; + templateData.commentText.innerHTML = ''; + const renderedComment = renderMarkdown(element.comment.body, { + inline: true, + actionHandler: { + callback: (content) => { + this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); + }, + disposeables: templateData.disposables + } + }); + + const images = renderedComment.getElementsByTagName('img'); + for (let i = 0; i < images.length; i++) { + const image = images[i]; + const textDescription = dom.$(''); + textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image"); + image.parentNode.replaceChild(textDescription, image); + } + + templateData.commentText.appendChild(renderedComment); } } 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 45268e182f1..3c2b37bcf3f 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/panel.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/panel.css @@ -30,8 +30,15 @@ opacity: 0.5; } +.comments-panel .comments-panel-container .tree-container .comment-container .text { + flex: 1; + min-width: 0; +} + .comments-panel .comments-panel-container .tree-container .comment-container .text * { margin: 0; + text-overflow: ellipsis; + overflow: hidden; } .comments-panel .comments-panel-container .message-box-container { @@ -49,6 +56,5 @@ .comments-panel .comments-panel-container .tree-container .comment-container { line-height: 22px; - text-overflow: ellipsis; - overflow: hidden; + margin-right: 5px; } \ No newline at end of file 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 9854ade1898..c3bd18b08e0 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -10,11 +10,8 @@ background-position: center center; } -.monaco-editor .comment-hint{ - display: flex; - align-items: center; - justify-content: center; - height: 16px; +.monaco-editor .comment-hint { + height: 20px; width: 20px; padding-left: 2px; background: url('comment.svg') center center no-repeat; @@ -38,6 +35,13 @@ display: flex; } +.monaco-editor .review-widget .body .review-comment blockquote { + margin: 0 7px 0 5px; + padding: 0 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; +} + .monaco-editor .review-widget .body .review-comment .avatar-container { margin-top: 4px !important; } @@ -55,6 +59,7 @@ .monaco-editor .review-widget .body .review-comment .review-comment-contents { margin-left: 20px; + user-select: text; } .monaco-editor .review-widget .body pre { @@ -112,6 +117,10 @@ padding: 0 0.4em; } +.monaco-editor .review-widget .body span { + white-space: pre; +} + .monaco-editor .review-widget .body .comment-body img { max-width: 100%; } @@ -121,6 +130,29 @@ padding: 8px 0; } +.monaco-editor .review-widget .body .comment-form .validation-error { + display: inline-block; + overflow: hidden; + text-align: left; + width: 100%; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -o-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + padding: 0.4em; + font-size: 12px; + line-height: 17px; + min-height: 34px; + margin-top: -1px; + margin-left: -1px; + word-wrap: break-word; +} + +.monaco-editor .review-widget .body .comment-form .validation-error.hidden { + display: none; +} + .monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button { display: none; } @@ -128,7 +160,7 @@ .monaco-editor .review-widget .body .comment-form.expand .monaco-editor, .monaco-editor .review-widget .body .comment-form.expand .form-actions { display: block; - box-sizing: border-box; + box-sizing: content-box; } .monaco-editor .review-widget .body .comment-form .review-thread-reply-button { @@ -136,7 +168,7 @@ display: block; width: 100%; resize: vertical; - border-radius: 3px; + border-radius: 0; box-sizing: border-box; padding: 6px 12px; font-weight: 600; @@ -144,6 +176,7 @@ white-space: nowrap; border: 0px; cursor: text; + outline: 1px solid transparent; } .monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus { @@ -158,6 +191,7 @@ max-height: 500px; border-radius: 3px; border: 0px; + padding: 6px 0 6px 12px; } .monaco-editor .review-widget .body .comment-form .form-actions { @@ -186,7 +220,7 @@ display: inline-block; font-size: 13px; margin-left: 20px; - cursor: pointer; + cursor: default; } .monaco-editor .review-widget .head .review-title .dirname:not(:empty) { @@ -233,4 +267,60 @@ .monaco-editor .review-widget>.body { border-top: 1px solid; position: relative; -} \ No newline at end of file +} + +.monaco-editor .comment-range-glyph { + margin-left: 5px; + width: 4px !important; + cursor: pointer; + z-index: 10; +} + +.monaco-editor .comment-range-glyph:before { + position: absolute; + content: ''; + height: 100%; + width: 0; + left: -2px; + transition: width 80ms linear, left 80ms linear; +} + +.monaco-editor .margin-view-overlays>div:hover>.comment-range-glyph.comment-diff-added:before { + position: absolute; + content: '+'; + height: 100%; + width: 9px; + left: -6px; + z-index: 10; + color: black; +} + +.monaco-editor .comment-range-glyph.comment-thread { + z-index: 20; +} + +.monaco-editor .comment-range-glyph.comment-thread:before { + position: absolute; + content: '◆'; + font-size: 10px; + height: 100%; + line-height: 100%; + width: 9px; + left: -6px; + z-index: 20; + color: black; + text-align: center; + display:flex; + flex-direction:row; + align-items: center; + justify-content: center; + +} + +.monaco-editor.inline-comment .margin-view-overlays .folding { + margin-left: 14px; +} + +.monaco-editor.inline-comment .margin-view-overlays .dirty-diff-glyph { + margin-left: 14px; +} diff --git a/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts b/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts index 3bbb410d3f5..a103105c641 100644 --- a/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts +++ b/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts @@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ICommandService } from 'vs/platform/commands/common/commands'; // Allowed Editor Contributions: -import { MenuPreventer } from 'vs/workbench/parts/codeEditor/electron-browser/menuPreventer'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; import { SelectionClipboard } from 'vs/workbench/parts/codeEditor/electron-browser/selectionClipboard'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; diff --git a/src/vs/workbench/parts/debug/browser/baseDebugView.ts b/src/vs/workbench/parts/debug/browser/baseDebugView.ts index e3cc797de24..dec53bb654a 100644 --- a/src/vs/workbench/parts/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/parts/debug/browser/baseDebugView.ts @@ -19,7 +19,6 @@ import { IControllerOptions } from 'vs/base/parts/tree/browser/treeDefaults'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -83,16 +82,16 @@ export function renderExpressionValue(expressionOrValue: IExpression | string, c } } - if (options.maxValueLength && value.length > options.maxValueLength) { + if (options.maxValueLength && value && value.length > options.maxValueLength) { value = value.substr(0, options.maxValueLength) + '...'; } if (value && !options.preserveWhitespace) { container.textContent = replaceWhitespace(value); } else { - container.textContent = value; + container.textContent = value || ''; } if (options.showHover) { - container.title = value; + container.title = value || ''; } } @@ -103,18 +102,15 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, dom.toggleClass(data.name, 'virtual', !!variable.presentationHint && variable.presentationHint.kind === 'virtual'); } - if (variable.value) { - data.name.textContent += (typeof variable.name === 'string') ? ':' : ''; - renderExpressionValue(variable, data.value, { - showChanged, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - preserveWhitespace: false, - showHover: true, - colorize: true - }); - } else { - data.value.textContent = ''; - data.value.title = ''; + renderExpressionValue(variable, data.value, { + showChanged, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + preserveWhitespace: false, + showHover: true, + colorize: true + }); + if (variable.value && typeof variable.name === 'string') { + data.name.textContent += ':'; } } @@ -154,11 +150,11 @@ export function renderRenameBox(debugService: IDebugService, contextViewService: if (renamed && element.value !== inputBox.value) { element.setVariable(inputBox.value) // if everything went fine we need to refresh ui elements since the variable update can change watch and variables view - .done(() => { + .then(() => { tree.refresh(element, false); // Need to force watch expressions to update since a variable change can have an effect on watches debugService.focusStackFrame(debugService.getViewModel().focusedStackFrame); - }, onUnexpectedError); + }); } } diff --git a/src/vs/workbench/parts/debug/browser/breakpointsView.ts b/src/vs/workbench/parts/debug/browser/breakpointsView.ts index 1879a7ad4a7..3703cf37902 100644 --- a/src/vs/workbench/parts/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/browser/breakpointsView.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { IAction, Action } from 'vs/base/common/actions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, EDITOR_CONTRIBUTION_ID, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugEditorContribution } from 'vs/workbench/parts/debug/common/debug'; import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint } from 'vs/workbench/parts/debug/common/debugModel'; @@ -16,14 +15,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Constants } from 'vs/editor/common/core/uint'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { getPathLabel } from 'vs/base/common/labels'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { basename } from 'vs/base/common/paths'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IListContextMenuEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { IEditor } from 'vs/workbench/common/editor'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -33,12 +28,20 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { ILabelService } from 'vs/platform/label/common/label'; const $ = dom.$; +function createCheckbox(): HTMLInputElement { + const checkbox = $('input'); + checkbox.type = 'checkbox'; + checkbox.tabIndex = -1; + + return checkbox; +} + export class BreakpointsView extends ViewletPanel { private static readonly MAX_VISIBLE_FILES = 9; @@ -58,7 +61,7 @@ export class BreakpointsView extends ViewletPanel { @IContextViewService private contextViewService: IContextViewService, @IConfigurationService configurationService: IConfigurationService ) { - super(options, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService); this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize(); this.settings = options.viewletSettings; @@ -86,19 +89,31 @@ export class BreakpointsView extends ViewletPanel { this.disposables.push(this.list.onOpen(e => { let isSingleClick = false; let isDoubleClick = false; + let isMiddleClick = false; let openToSide = false; const browserEvent = e.browserEvent; if (browserEvent instanceof MouseEvent) { isSingleClick = browserEvent.detail === 1; isDoubleClick = browserEvent.detail === 2; + isMiddleClick = browserEvent.button === 1; openToSide = (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey); } const focused = this.list.getFocusedElements(); const element = focused.length ? focused[0] : undefined; + + if (isMiddleClick) { + if (element instanceof Breakpoint) { + this.debugService.removeBreakpoints(element.getId()); + } else if (element instanceof FunctionBreakpoint) { + this.debugService.removeFunctionBreakpoints(element.getId()); + } + return; + } + if (element instanceof Breakpoint) { - openBreakpointSource(element, openToSide, isSingleClick, this.debugService, this.editorService).done(undefined, onUnexpectedError); + openBreakpointSource(element, openToSide, isSingleClick, this.debugService, this.editorService); } if (isDoubleClick && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedFunctionBreakpoint()) { this.debugService.getViewModel().setSelectedFunctionBreakpoint(element); @@ -221,7 +236,7 @@ export class BreakpointsView extends ViewletPanel { } } -class BreakpointsDelegate implements IDelegate { +class BreakpointsDelegate implements IVirtualDelegate { constructor(private debugService: IDebugService) { // noop @@ -279,9 +294,7 @@ class BreakpointsRenderer implements IRenderer$('input'); - data.checkbox.type = 'checkbox'; + data.checkbox = createCheckbox(); data.toDispose = []; data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); @@ -320,29 +332,28 @@ class BreakpointsRenderer implements IRenderer$('input'); - data.checkbox.type = 'checkbox'; + data.checkbox = createCheckbox(); data.toDispose = []; data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); @@ -388,6 +398,10 @@ class ExceptionBreakpointsRenderer implements IRenderer { constructor( - @IDebugService private debugService: IDebugService, - @ITextFileService private textFileService: ITextFileService + @IDebugService private debugService: IDebugService ) { // noop } @@ -413,8 +426,7 @@ class FunctionBreakpointsRenderer implements IRenderer$('input'); - data.checkbox.type = 'checkbox'; + data.checkbox = createCheckbox(); data.toDispose = []; data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); @@ -431,7 +443,7 @@ class FunctionBreakpointsRenderer implements IRenderer { - if (!template.breakpoint.name) { - wrapUp(true); - } + // Need to react with a timeout on the blur event due to possible concurent splices #56443 + setTimeout(() => { + if (!template.breakpoint.name) { + wrapUp(true); + } + }); })); template.inputBox = inputBox; @@ -516,6 +535,10 @@ class FunctionBreakpointInputRenderer implements IRenderer { this.start.blur(); - this.actionRunner.run(this.action, this.context).done(null, errors.onUnexpectedError); + this.actionRunner.run(this.action, this.context); })); this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => { @@ -94,7 +94,7 @@ export class StartDebugActionItem implements IActionItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter)) { - this.actionRunner.run(this.action, this.context).done(null, errors.onUnexpectedError); + this.actionRunner.run(this.action, this.context); } if (event.equals(KeyCode.RightArrow)) { this.selectBox.focus(); @@ -177,7 +177,7 @@ export class StartDebugActionItem implements IActionItem { const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); this.options.push({ label, handler: () => { - this.commandService.executeCommand('debug.addConfiguration', l.uri.toString()).done(undefined, errors.onUnexpectedError); + this.commandService.executeCommand('debug.addConfiguration', l.uri.toString()); return false; } }); @@ -194,7 +194,7 @@ export class FocusSessionActionItem extends SelectActionItem { @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, [], -1, contextViewService); + super(null, action, [], -1, contextViewService, { ariaLabel: nls.localize('debugSession', 'Debug Session') }); this.toDispose.push(attachSelectBoxStyler(this.selectBox, themeService)); diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index d1f29cf4eed..bcc80595b08 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -9,11 +9,12 @@ import * as lifecycle from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import * as aria from 'vs/base/browser/ui/aria/aria'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { IDebugService, State, ISession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID, SessionState } +import { IDebugService, State, IDebugSession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID } from 'vs/workbench/parts/debug/common/debug'; -import { Variable, Expression, Thread, Breakpoint, Session } from 'vs/workbench/parts/debug/common/debugModel'; +import { Variable, Expression, Thread, Breakpoint } from 'vs/workbench/parts/debug/common/debugModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -25,6 +26,7 @@ import { ITree } from 'vs/base/parts/tree/browser/tree'; import { first } from 'vs/base/common/arrays'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { memoize } from 'vs/base/common/decorators'; +import { Schemas } from 'vs/base/common/network'; export abstract class AbstractDebugAction extends Action { @@ -112,7 +114,7 @@ export class ConfigureAction extends AbstractDebugAction { configurationManager.selectConfiguration(configurationManager.getLaunches()[0]); } - return configurationManager.selectedConfiguration.launch.openConfigFile(sideBySide); + return configurationManager.selectedConfiguration.launch.openConfigFile(sideBySide, false); } } @@ -136,12 +138,12 @@ export class StartAction extends AbstractDebugAction { public run(): TPromise { const configurationManager = this.debugService.getConfigurationManager(); let launch = configurationManager.selectedConfiguration.launch; - if (!launch) { - const rootUri = this.historyService.getLastActiveWorkspaceRoot(); + if (!launch || launch.getConfigurationNames().length === 0) { + const rootUri = this.historyService.getLastActiveWorkspaceRoot(Schemas.file); launch = configurationManager.getLaunch(rootUri); if (!launch || launch.getConfigurationNames().length === 0) { const launches = configurationManager.getLaunches(); - launch = first(launches, l => !!l.getConfigurationNames().length, launches.length ? launches[0] : launch); + launch = first(launches, l => !!l.getConfigurationNames().length, launch); } configurationManager.selectConfiguration(launch); @@ -224,12 +226,12 @@ export class RestartAction extends AbstractDebugAction { return new StartAction(StartAction.ID, StartAction.LABEL, this.debugService, this.keybindingService, this.contextService, this.historyService); } - private setLabel(session: ISession): void { - this.updateLabel(session && session.state === SessionState.ATTACH ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); + private setLabel(session: IDebugSession): void { + this.updateLabel(session && session.configuration.request === 'attach' ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); } - public run(session: ISession): TPromise { - if (!(session instanceof Session)) { + public run(session: IDebugSession): TPromise { + if (!session || !session.getId) { session = this.debugService.getViewModel().focusedSession; } @@ -323,8 +325,8 @@ export class StopAction extends AbstractDebugAction { super(id, label, 'debug-action stop', debugService, keybindingService, 80); } - public run(session: ISession): TPromise { - if (!(session instanceof Session)) { + public run(session: IDebugSession): TPromise { + if (!session || !session.getId) { session = this.debugService.getViewModel().focusedSession; } @@ -587,7 +589,7 @@ export class SetValueAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const session = this.debugService.getViewModel().focusedSession; - return super.isEnabled(state) && state === State.Stopped && session && session.raw.capabilities.supportsSetVariable; + return super.isEnabled(state) && state === State.Stopped && session && session.capabilities.supportsSetVariable; } } @@ -691,6 +693,7 @@ export class ClearReplAction extends AbstractDebugAction { public run(): TPromise { this.debugService.removeReplExpressions(); + aria.status(nls.localize('debugConsoleCleared', "Debug console was cleared")); // focus back to repl return this.panelService.openPanel(REPL_ID, true); @@ -800,7 +803,7 @@ export class StepBackAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const session = this.debugService.getViewModel().focusedSession; return super.isEnabled(state) && state === State.Stopped && - session && session.raw.capabilities.supportsStepBack; + session && session.capabilities.supportsStepBack; } } @@ -823,7 +826,7 @@ export class ReverseContinueAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const session = this.debugService.getViewModel().focusedSession; return super.isEnabled(state) && state === State.Stopped && - session && session.raw.capabilities.supportsStepBack; + session && session.capabilities.supportsStepBack; } } diff --git a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts index 15a5f4e83a9..1733710138a 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts @@ -5,9 +5,7 @@ import 'vs/css!./media/debugActionsWidget'; import * as errors from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; import * as browser from 'vs/base/browser/browser'; -import { $, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -31,8 +29,10 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { isExtensionHostDebugging } from 'vs/workbench/parts/debug/common/debugUtils'; const DEBUG_ACTIONS_WIDGET_POSITION_KEY = 'debug.actionswidgetposition'; +const DEBUG_ACTIONS_WIDGET_Y_KEY = 'debug.actionswidgety'; export const debugToolBarBackground = registerColor('debugToolBar.background', { dark: '#333333', @@ -47,8 +47,8 @@ export const debugToolBarBorder = registerColor('debugToolBar.border', { export class DebugActionsWidget extends Themable implements IWorkbenchContribution { - private $el: Builder; - private dragArea: Builder; + private $el: HTMLElement; + private dragArea: HTMLElement; private actionBar: ActionBar; private allActions: AbstractDebugAction[] = []; private activeActions: AbstractDebugAction[]; @@ -71,15 +71,15 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi ) { super(themeService); - this.$el = $().div().addClass('debug-actions-widget').style('top', `${partService.getTitleBarOffset()}px`); - this.dragArea = $().div().addClass('drag-area'); - this.$el.append(this.dragArea); + this.$el = dom.$('div.debug-actions-widget'); + this.$el.style.top = `${partService.getTitleBarOffset()}px`; - const actionBarContainter = $().div().addClass('.action-bar-container'); - this.$el.append(actionBarContainter); + this.dragArea = dom.append(this.$el, dom.$('div.drag-area')); + + const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container')); this.activeActions = []; - this.actionBar = new ActionBar(actionBarContainter.getHTMLElement(), { + this.actionBar = this._register(new ActionBar(actionBarContainer, { orientation: ActionsOrientation.HORIZONTAL, actionItemProvider: (action: IAction) => { if (action.id === FocusSessionAction.ID) { @@ -88,28 +88,26 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi return null; } - }); + })); - this.updateScheduler = new RunOnceScheduler(() => { + this.updateScheduler = this._register(new RunOnceScheduler(() => { const state = this.debugService.state; const toolBarLocation = this.configurationService.getValue('debug').toolBarLocation; - if (state === State.Inactive || this.configurationService.getValue('debug').hideActionBar - || toolBarLocation === 'docked' || toolBarLocation === 'hidden') { + if (state === State.Inactive || state === State.Initializing || toolBarLocation === 'docked' || toolBarLocation === 'hidden') { return this.hide(); } - const actions = DebugActionsWidget.getActions(this.allActions, this.toUnbind, this.debugService, this.keybindingService, this.instantiationService); + const actions = DebugActionsWidget.getActions(this.allActions, this.toDispose, this.debugService, this.keybindingService, this.instantiationService); if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id)) { this.actionBar.clear(); this.actionBar.push(actions, { icon: true, label: false }); this.activeActions = actions; } this.show(); - }, 20); + }, 20)); this.updateStyles(); - this.toUnbind.push(this.actionBar); this.registerListeners(); this.hide(); @@ -117,10 +115,10 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi } private registerListeners(): void { - this.toUnbind.push(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); - this.toUnbind.push(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); - this.toUnbind.push(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { + this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); + this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); + this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error if (e.error && !errors.isPromiseCanceledError(e.error)) { this.notificationService.error(e.error); @@ -137,85 +135,95 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'debugActionsWidget' }); } })); - $(window).on(dom.EventType.RESIZE, () => this.setXCoordinate(), this.toUnbind); + this._register(dom.addDisposableListener(window, dom.EventType.RESIZE, () => this.setCoordinates())); - this.dragArea.on(dom.EventType.MOUSE_UP, (event: MouseEvent) => { + this._register(dom.addDisposableListener(this.dragArea, dom.EventType.MOUSE_UP, (event: MouseEvent) => { const mouseClickEvent = new StandardMouseEvent(event); if (mouseClickEvent.detail === 2) { // double click on debug bar centers it again #8250 - const widgetWidth = this.$el.getHTMLElement().clientWidth; - this.setXCoordinate(0.5 * window.innerWidth - 0.5 * widgetWidth); + const widgetWidth = this.$el.clientWidth; + this.setCoordinates(0.5 * window.innerWidth - 0.5 * widgetWidth, 0); this.storePosition(); } - }); + })); - this.dragArea.on(dom.EventType.MOUSE_DOWN, (event: MouseEvent) => { - const $window = $(window); - this.dragArea.addClass('dragged'); + this._register(dom.addDisposableListener(this.dragArea, dom.EventType.MOUSE_DOWN, (event: MouseEvent) => { + dom.addClass(this.dragArea, 'dragged'); - $window.on('mousemove', (e: MouseEvent) => { + const mouseMoveListener = dom.addDisposableListener(window, 'mousemove', (e: MouseEvent) => { const mouseMoveEvent = new StandardMouseEvent(e); // Prevent default to stop editor selecting text #8524 mouseMoveEvent.preventDefault(); // Reduce x by width of drag handle to reduce jarring #16604 - this.setXCoordinate(mouseMoveEvent.posx - 14); - }).once('mouseup', (e: MouseEvent) => { - this.storePosition(); - this.dragArea.removeClass('dragged'); - $window.off('mousemove'); + this.setCoordinates(mouseMoveEvent.posx - 14, mouseMoveEvent.posy - this.partService.getTitleBarOffset()); }); - }); - this.toUnbind.push(this.partService.onTitleBarVisibilityChange(() => this.positionDebugWidget())); - this.toUnbind.push(browser.onDidChangeZoomLevel(() => this.positionDebugWidget())); + const mouseUpListener = dom.addDisposableListener(window, 'mouseup', (e: MouseEvent) => { + this.storePosition(); + dom.removeClass(this.dragArea, 'dragged'); + + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + + this._register(this.partService.onTitleBarVisibilityChange(() => this.setYCoordinate())); + this._register(browser.onDidChangeZoomLevel(() => this.setYCoordinate())); } private storePosition(): void { - const position = parseFloat(this.$el.getComputedStyle().left) / window.innerWidth; - this.storageService.store(DEBUG_ACTIONS_WIDGET_POSITION_KEY, position, StorageScope.WORKSPACE); + const position = parseFloat(dom.getComputedStyle(this.$el).left) / window.innerWidth; + this.storageService.store(DEBUG_ACTIONS_WIDGET_POSITION_KEY, position, StorageScope.GLOBAL); } protected updateStyles(): void { super.updateStyles(); if (this.$el) { - this.$el.style('background-color', this.getColor(debugToolBarBackground)); + this.$el.style.backgroundColor = this.getColor(debugToolBarBackground); const widgetShadowColor = this.getColor(widgetShadow); - this.$el.style('box-shadow', widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : null); + this.$el.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : null; const contrastBorderColor = this.getColor(contrastBorder); const borderColor = this.getColor(debugToolBarBorder); if (contrastBorderColor) { - this.$el.style('border', `1px solid ${contrastBorderColor}`); + this.$el.style.border = `1px solid ${contrastBorderColor}`; } else { - this.$el.style({ - 'border': borderColor ? `solid ${borderColor}` : 'none', - 'border-width': '1px 0' - }); + this.$el.style.border = borderColor ? `solid ${borderColor}` : 'none'; + this.$el.style.border = '1px 0'; } } } - private positionDebugWidget(): void { + private setYCoordinate(y = 0): void { const titlebarOffset = this.partService.getTitleBarOffset(); - - $(this.$el).style('top', `${titlebarOffset}px`); + this.$el.style.top = `${titlebarOffset + y}px`; } - private setXCoordinate(x?: number): void { + private setCoordinates(x?: number, y?: number): void { if (!this.isVisible) { return; } - const widgetWidth = this.$el.getHTMLElement().clientWidth; + const widgetWidth = this.$el.clientWidth; if (x === undefined) { - const positionPercentage = this.storageService.get(DEBUG_ACTIONS_WIDGET_POSITION_KEY, StorageScope.WORKSPACE); + const positionPercentage = this.storageService.get(DEBUG_ACTIONS_WIDGET_POSITION_KEY, StorageScope.GLOBAL); x = positionPercentage !== undefined ? parseFloat(positionPercentage) * window.innerWidth : (0.5 * window.innerWidth - 0.5 * widgetWidth); } x = Math.max(0, Math.min(x, window.innerWidth - widgetWidth)); // do not allow the widget to overflow on the right - this.$el.style('left', `${x}px`); + this.$el.style.left = `${x}px`; + + if (y === undefined) { + y = this.storageService.getInteger(DEBUG_ACTIONS_WIDGET_Y_KEY, StorageScope.GLOBAL, 0); + } + const titleAreaHeight = 35; + if ((y < titleAreaHeight / 2) || (y > titleAreaHeight + titleAreaHeight / 2)) { + const moveToTop = y < titleAreaHeight; + this.setYCoordinate(moveToTop ? 0 : titleAreaHeight); + this.storageService.store(DEBUG_ACTIONS_WIDGET_Y_KEY, moveToTop ? 0 : 2 * titleAreaHeight, StorageScope.GLOBAL); + } } private onDidConfigurationChange(event: IConfigurationChangeEvent): void { @@ -226,24 +234,25 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi private show(): void { if (this.isVisible) { + this.setCoordinates(); return; } if (!this.isBuilt) { this.isBuilt = true; - this.$el.build(document.getElementById(this.partService.getWorkbenchElementId())); + this.partService.getWorkbenchElement().appendChild(this.$el); } this.isVisible = true; - this.$el.show(); - this.setXCoordinate(); + dom.show(this.$el); + this.setCoordinates(); } private hide(): void { this.isVisible = false; - this.$el.hide(); + dom.hide(this.$el); } - public static getActions(allActions: AbstractDebugAction[], toUnbind: IDisposable[], debugService: IDebugService, keybindingService: IKeybindingService, instantiationService: IInstantiationService): AbstractDebugAction[] { + public static getActions(allActions: AbstractDebugAction[], toDispose: IDisposable[], debugService: IDebugService, keybindingService: IKeybindingService, instantiationService: IInstantiationService): AbstractDebugAction[] { if (allActions.length === 0) { allActions.push(new ContinueAction(ContinueAction.ID, ContinueAction.LABEL, debugService, keybindingService)); allActions.push(new PauseAction(PauseAction.ID, PauseAction.LABEL, debugService, keybindingService)); @@ -256,14 +265,12 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi allActions.push(new StepBackAction(StepBackAction.ID, StepBackAction.LABEL, debugService, keybindingService)); allActions.push(new ReverseContinueAction(ReverseContinueAction.ID, ReverseContinueAction.LABEL, debugService, keybindingService)); allActions.push(instantiationService.createInstance(FocusSessionAction, FocusSessionAction.ID, FocusSessionAction.LABEL)); - allActions.forEach(a => { - toUnbind.push(a); - }); + allActions.forEach(a => toDispose.push(a)); } const state = debugService.state; const session = debugService.getViewModel().focusedSession; - const attached = session && session.configuration.request === 'attach' && session.configuration.type && !strings.equalsIgnoreCase(session.configuration.type, 'extensionHost'); + const attached = session && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration); return allActions.filter(a => { if (a.id === ContinueAction.ID) { @@ -273,10 +280,10 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi return state === State.Running; } if (a.id === StepBackAction.ID) { - return session && session.raw.capabilities.supportsStepBack; + return session && session.capabilities.supportsStepBack; } if (a.id === ReverseContinueAction.ID) { - return session && session.raw.capabilities.supportsStepBack; + return session && session.capabilities.supportsStepBack; } if (a.id === DisconnectAction.ID) { return attached; @@ -296,7 +303,7 @@ export class DebugActionsWidget extends Themable implements IWorkbenchContributi super.dispose(); if (this.$el) { - this.$el.destroy(); + this.$el.remove(); delete this.$el; } } diff --git a/src/vs/workbench/parts/debug/browser/debugCommands.ts b/src/vs/workbench/parts/debug/browser/debugCommands.ts index 8a36c1e3150..c9c77ba7795 100644 --- a/src/vs/workbench/parts/debug/browser/debugCommands.ts +++ b/src/vs/workbench/parts/debug/browser/debugCommands.ts @@ -7,11 +7,10 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { TPromise } from 'vs/base/common/winjs.base'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import * as errors from 'vs/base/common/errors'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED } from 'vs/workbench/parts/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint } from 'vs/workbench/parts/debug/common/debugModel'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -23,14 +22,16 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { openBreakpointSource } from 'vs/workbench/parts/debug/browser/breakpointsView'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { InputFocusedContext } from 'vs/platform/workbench/common/contextkeys'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; +export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; + export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.toggleBreakpoint', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(5), + weight: KeybindingWeight.WorkbenchContrib + 5, when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, InputFocusedContext.toNegated()), primary: KeyCode.Space, handler: (accessor) => { @@ -40,7 +41,7 @@ export function registerCommands(): void { if (list instanceof List) { const focused = list.getFocusedElements(); if (focused && focused.length) { - debugService.enableOrDisableBreakpoints(!focused[0].enabled, focused[0]).done(null, errors.onUnexpectedError); + debugService.enableOrDisableBreakpoints(!focused[0].enabled, focused[0]); } } } @@ -48,7 +49,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.enableOrDisableBreakpoint', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, primary: undefined, when: EditorContextKeys.editorTextFocus, handler: (accessor) => { @@ -61,7 +62,7 @@ export function registerCommands(): void { const position = widget.getPosition(); const bps = debugService.getModel().getBreakpoints({ uri: model.uri, lineNumber: position.lineNumber }); if (bps.length) { - debugService.enableOrDisableBreakpoints(!bps[0].enabled, bps[0]).done(null, errors.onUnexpectedError); + debugService.enableOrDisableBreakpoints(!bps[0].enabled, bps[0]); } } } @@ -70,7 +71,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.renameWatchExpression', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(5), + weight: KeybindingWeight.WorkbenchContrib + 5, when: CONTEXT_WATCH_EXPRESSIONS_FOCUSED, primary: KeyCode.F2, mac: { primary: KeyCode.Enter }, @@ -91,7 +92,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.setVariable', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(5), + weight: KeybindingWeight.WorkbenchContrib + 5, when: CONTEXT_VARIABLES_FOCUSED, primary: KeyCode.F2, mac: { primary: KeyCode.Enter }, @@ -112,7 +113,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.removeWatchExpression', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_EXPRESSION_SELECTED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, @@ -133,7 +134,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.removeBreakpoint', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_SELECTED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, @@ -147,9 +148,9 @@ export function registerCommands(): void { const focused = list.getFocusedElements(); const element = focused.length ? focused[0] : undefined; if (element instanceof Breakpoint) { - debugService.removeBreakpoints(element.getId()).done(null, errors.onUnexpectedError); + debugService.removeBreakpoints(element.getId()); } else if (element instanceof FunctionBreakpoint) { - debugService.removeFunctionBreakpoints(element.getId()).done(null, errors.onUnexpectedError); + debugService.removeFunctionBreakpoints(element.getId()); } } } @@ -157,7 +158,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.installAdditionalDebuggers', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, handler: (accessor) => { @@ -172,8 +173,8 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.addConfiguration', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + id: ADD_CONFIGURATION_ID, + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, handler: (accessor, launchUri: string) => { @@ -184,7 +185,7 @@ export function registerCommands(): void { } const launch = manager.getLaunches().filter(l => l.uri.toString() === launchUri).pop() || manager.selectedConfiguration.launch; - return launch.openConfigFile(false).done(({ editor, created }) => { + return launch.openConfigFile(false, false).then(({ editor, created }) => { if (editor && !created) { const codeEditor = editor.getControl(); if (codeEditor) { @@ -197,7 +198,6 @@ export function registerCommands(): void { } }); - const INLINE_BREAKPOINT_COMMAND_ID = 'editor.debug.action.toggleInlineBreakpoint'; const inlineBreakpointHandler = (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); const editorService = accessor.get(IEditorService); @@ -219,37 +219,33 @@ export function registerCommands(): void { return TPromise.as(null); }; KeybindingsRegistry.registerCommandAndKeybindingRule({ - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Shift | KeyCode.F9, when: EditorContextKeys.editorTextFocus, - id: INLINE_BREAKPOINT_COMMAND_ID, - handler: inlineBreakpointHandler - }); - CommandsRegistry.registerCommand({ - id: 'editor.debug.action.toggleColumnBreakpoint', + id: TOGGLE_INLINE_BREAKPOINT_ID, handler: inlineBreakpointHandler }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { - id: INLINE_BREAKPOINT_COMMAND_ID, + id: TOGGLE_INLINE_BREAKPOINT_ID, title: nls.localize('inlineBreakpoint', "Inline Breakpoint"), category: nls.localize('debug', "Debug") } }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { command: { - id: INLINE_BREAKPOINT_COMMAND_ID, + id: TOGGLE_INLINE_BREAKPOINT_ID, title: nls.localize('addInlineBreakpoint', "Add Inline Breakpoint") }, - when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, EditorContextKeys.writable), + when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.writable, EditorContextKeys.editorTextFocus), group: 'debug', order: 1 }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.openBreakpointToSide', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: CONTEXT_BREAKPOINTS_FOCUSED, primary: KeyMod.CtrlCmd | KeyCode.Enter, secondary: [KeyMod.Alt | KeyCode.Enter], diff --git a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts index f7370887789..ba1059a13ca 100644 --- a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts +++ b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { guessMimeTypes, MIME_TEXT } from 'vs/base/common/mime'; @@ -12,7 +12,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { DEBUG_SCHEME, IDebugService, ISession } from 'vs/workbench/parts/debug/common/debug'; +import { DEBUG_SCHEME, IDebugService, IDebugSession } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; /** @@ -41,13 +41,11 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC public provideTextContent(resource: uri): TPromise { - let session: ISession; - let sourceRef: number; + let session: IDebugSession; if (resource.query) { const data = Source.getEncodedDebugData(resource); session = this.debugService.getModel().getSessions().filter(p => p.getId() === data.sessionId).pop(); - sourceRef = data.sourceReference; } if (!session) { @@ -58,39 +56,21 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC if (!session) { return TPromise.wrapError(new Error(localize('unable', "Unable to resolve the resource without a debug session"))); } - const source = session.getSourceForUri(resource); - let rawSource: DebugProtocol.Source; - if (source) { - rawSource = source.raw; - if (!sourceRef) { - sourceRef = source.reference; - } - } else { - // create a Source - rawSource = { - path: resource.with({ scheme: '', query: '' }).toString(true), // Remove debug: scheme - sourceReference: sourceRef - }; - } - const createErrModel = (message: string) => { this.debugService.sourceIsNotAvailable(resource); const modePromise = this.modeService.getOrCreateMode(MIME_TEXT); - const model = this.modelService.createModel(message, modePromise, resource); - - return model; + return this.modelService.createModel(message, modePromise, resource); }; - return session.raw.source({ sourceReference: sourceRef, source: rawSource }).then(response => { + return session.loadSource(resource).then(response => { if (!response) { return createErrModel(localize('canNotResolveSource', "Could not resolve resource {0}, no response from debug extension.", resource.toString())); } const mime = response.body.mimeType || guessMimeTypes(resource.path)[0]; const modePromise = this.modeService.getOrCreateMode(mime); - const model = this.modelService.createModel(response.body.content, modePromise, resource); + return this.modelService.createModel(response.body.content, modePromise, resource); - return model; }, (err: DebugProtocol.ErrorResponse) => createErrModel(err.message)); } } diff --git a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts index 4c871d8342b..3d00826481d 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts @@ -10,23 +10,26 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint } from 'vs/workbench/parts/debug/common/debug'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/parts/debug/browser/breakpointsView'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; class ToggleBreakpointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.toggleBreakpoint', + id: TOGGLE_BREAKPOINT_ID, label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), alias: 'Debug: Toggle Breakpoint', precondition: null, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyCode.F9 + primary: KeyCode.F9, + weight: KeybindingWeight.EditorContrib } }); } @@ -49,11 +52,12 @@ class ToggleBreakpointAction extends EditorAction { } } +export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint'; class ConditionalBreakpointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.conditionalBreakpoint', + id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), alias: 'Debug: Add Conditional Breakpoint...', precondition: null @@ -70,11 +74,12 @@ class ConditionalBreakpointAction extends EditorAction { } } +export const TOGGLE_LOG_POINT_ID = 'editor.debug.action.toggleLogPoint'; class LogPointAction extends EditorAction { constructor() { super({ - id: 'editor.debug.action.toggleLogPoint', + id: TOGGLE_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', precondition: null @@ -98,7 +103,7 @@ class RunToCursorAction extends EditorAction { id: 'editor.debug.action.runToCursor', label: nls.localize('runToCursor', "Run to Cursor"), alias: 'Debug: Run to Cursor', - precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL, EditorContextKeys.writable, CONTEXT_DEBUG_STATE.isEqualTo('stopped')), + precondition: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, EditorContextKeys.writable, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 2 @@ -113,7 +118,7 @@ class RunToCursorAction extends EditorAction { } let breakpointToRemove: IBreakpoint; - const oneTimeListener = debugService.getViewModel().focusedSession.raw.onDidEvent(event => { + const oneTimeListener = debugService.getViewModel().focusedSession.onDidCustomEvent(event => { if (event.event === 'stopped' || event.event === 'exit') { if (breakpointToRemove) { debugService.removeBreakpoints(breakpointToRemove.getId()); @@ -141,7 +146,7 @@ class SelectionToReplAction extends EditorAction { id: 'editor.debug.action.selectionToRepl', label: nls.localize('debugEvaluate', "Debug: Evaluate"), alias: 'Debug: Evaluate', - precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL), + precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 0 @@ -167,7 +172,7 @@ class SelectionToWatchExpressionsAction extends EditorAction { id: 'editor.debug.action.selectionToWatch', label: nls.localize('debugAddToWatch', "Debug: Add to Watch"), alias: 'Debug: Add to Watch', - precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, CONTEXT_NOT_IN_DEBUG_REPL), + precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), menuOpts: { group: 'debug', order: 1 @@ -194,7 +199,8 @@ class ShowDebugHoverAction extends EditorAction { precondition: CONTEXT_IN_DEBUG_MODE, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I), + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 4b21fa882fb..185e38174a3 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -12,7 +12,6 @@ import { IDebugService, IBreakpoint, State, IBreakpointUpdateData } from 'vs/wor import { IModelService } from 'vs/editor/common/services/modelService'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { getBreakpointMessageAndClassName } from 'vs/workbench/parts/debug/browser/breakpointsView'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IBreakpointDecoration { decorationId: string; @@ -38,7 +37,6 @@ export class DebugEditorModelManager implements IWorkbenchContribution { constructor( @IModelService private modelService: IModelService, @IDebugService private debugService: IDebugService, - @ITextFileService private textFileService: ITextFileService ) { this.modelDataMap = new Map(); this.toDispose = []; @@ -277,7 +275,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } private getBreakpointDecorationOptions(breakpoint: IBreakpoint): IModelDecorationOptions { - const { className, message } = getBreakpointMessageAndClassName(this.debugService, this.textFileService, breakpoint); + const { className, message } = getBreakpointMessageAndClassName(this.debugService, breakpoint); let glyphMarginHoverMessage: MarkdownString; if (message) { diff --git a/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts b/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts index 66905ae107a..6661ad6ffb9 100644 --- a/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts +++ b/src/vs/workbench/parts/debug/browser/debugQuickOpen.ts @@ -11,10 +11,10 @@ import * as QuickOpen from 'vs/base/parts/quickopen/common/quickOpen'; import * as Model from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IDebugService, ILaunch } from 'vs/workbench/parts/debug/common/debug'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import * as errors from 'vs/base/common/errors'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StartAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CancellationToken } from 'vs/base/common/cancellation'; class AddConfigEntry extends Model.QuickOpenEntry { @@ -38,7 +38,7 @@ class AddConfigEntry extends Model.QuickOpenEntry { if (mode === QuickOpen.Mode.PREVIEW) { return false; } - this.commandService.executeCommand('debug.addConfiguration', this.launch.uri.toString()).done(undefined, errors.onUnexpectedError); + this.commandService.executeCommand('debug.addConfiguration', this.launch.uri.toString()); return true; } @@ -68,7 +68,7 @@ class StartDebugEntry extends Model.QuickOpenEntry { } // Run selected debug configuration this.debugService.getConfigurationManager().selectConfiguration(this.launch, this.configurationName); - this.debugService.startDebugging(this.launch).done(undefined, e => this.notificationService.error(e)); + this.debugService.startDebugging(this.launch).then(undefined, e => this.notificationService.error(e)); return true; } @@ -77,6 +77,7 @@ class StartDebugEntry extends Model.QuickOpenEntry { export class DebugQuickOpenHandler extends Quickopen.QuickOpenHandler { public static readonly ID = 'workbench.picker.launch'; + private autoFocusIndex: number; constructor( @@ -92,13 +93,13 @@ export class DebugQuickOpenHandler extends Quickopen.QuickOpenHandler { return nls.localize('debugAriaLabel', "Type a name of a launch configuration to run."); } - public getResults(input: string): TPromise { + public getResults(input: string, token: CancellationToken): TPromise { const configurations: Model.QuickOpenEntry[] = []; const configManager = this.debugService.getConfigurationManager(); const launches = configManager.getLaunches(); for (let launch of launches) { - launch.getConfigurationNames().map(config => ({ config: config, highlights: Filters.matchesContiguousSubString(input, config) })) + launch.getConfigurationNames().map(config => ({ config: config, highlights: Filters.matchesFuzzy(input, config, true) })) .filter(({ highlights }) => !!highlights) .forEach(({ config, highlights }) => { if (launch === configManager.selectedConfiguration.launch && config === configManager.selectedConfiguration.name) { @@ -110,7 +111,7 @@ export class DebugQuickOpenHandler extends Quickopen.QuickOpenHandler { launches.filter(l => !l.hidden).forEach((l, index) => { const label = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); - const entry = new AddConfigEntry(label, l, this.commandService, this.contextService, Filters.matchesContiguousSubString(input, label)); + const entry = new AddConfigEntry(label, l, this.commandService, this.contextService, Filters.matchesFuzzy(input, label, true)); if (index === 0) { configurations.push(new Model.QuickOpenEntryGroup(entry, undefined, true)); } else { diff --git a/src/vs/workbench/parts/debug/browser/debugStatus.ts b/src/vs/workbench/parts/debug/browser/debugStatus.ts index f72afca4b5d..4ada0c1e5f4 100644 --- a/src/vs/workbench/parts/debug/browser/debugStatus.ts +++ b/src/vs/workbench/parts/debug/browser/debugStatus.ts @@ -5,8 +5,7 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import * as errors from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; @@ -18,7 +17,6 @@ import { STATUS_BAR_DEBUGGING_FOREGROUND, isStatusbarInDebugMode } from 'vs/work const $ = dom.$; export class DebugStatus extends Themable implements IStatusbarItem { - private toDispose: IDisposable[]; private container: HTMLElement; private statusBarItem: HTMLElement; private label: HTMLElement; @@ -32,28 +30,23 @@ export class DebugStatus extends Themable implements IStatusbarItem { @IConfigurationService configurationService: IConfigurationService ) { super(themeService); - this.toDispose = []; - this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(e => { + this._register(this.debugService.getConfigurationManager().onDidSelectConfiguration(e => { this.setLabel(); })); - this.toDispose.push(this.debugService.onDidChangeState(state => { + this._register(this.debugService.onDidChangeState(state => { if (state !== State.Inactive && this.showInStatusBar === 'onFirstSessionStart') { this.doRender(); } })); this.showInStatusBar = configurationService.getValue('debug').showInStatusBar; - this.toDispose.push(configurationService.onDidChangeConfiguration(e => { + this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('debug.showInStatusBar')) { this.showInStatusBar = configurationService.getValue('debug').showInStatusBar; - if (this.showInStatusBar === 'never' && this.statusBarItem) { - this.statusBarItem.hidden = true; - } else { - if (this.statusBarItem) { - this.statusBarItem.hidden = false; - } - if (this.showInStatusBar === 'always') { - this.doRender(); - } + if (this.showInStatusBar === 'always') { + this.doRender(); + } + if (this.statusBarItem) { + dom.toggleClass(this.statusBarItem, 'hidden', this.showInStatusBar === 'never'); } } })); @@ -82,9 +75,7 @@ export class DebugStatus extends Themable implements IStatusbarItem { private doRender(): void { if (!this.statusBarItem && this.container) { this.statusBarItem = dom.append(this.container, $('.debug-statusbar-item')); - this.toDispose.push(dom.addDisposableListener(this.statusBarItem, 'click', () => { - this.quickOpenService.show('debug ').done(undefined, errors.onUnexpectedError); - })); + this._register(dom.addDisposableListener(this.statusBarItem, 'click', () => this.quickOpenService.show('debug '))); this.statusBarItem.title = nls.localize('selectAndStartDebug', "Select and start debug configuration"); const a = dom.append(this.statusBarItem, $('a')); this.icon = dom.append(a, $('.icon')); @@ -99,17 +90,11 @@ export class DebugStatus extends Themable implements IStatusbarItem { if (this.label && this.statusBarItem) { const manager = this.debugService.getConfigurationManager(); const name = manager.selectedConfiguration.name; - if (name && manager.selectedConfiguration.launch) { - this.statusBarItem.style.display = 'block'; + const nameAndLaunchPresent = name && manager.selectedConfiguration.launch; + dom.toggleClass(this.statusBarItem, 'hidden', this.showInStatusBar === 'never' || !nameAndLaunchPresent); + if (nameAndLaunchPresent) { this.label.textContent = manager.getLaunches().length > 1 ? `${name} (${manager.selectedConfiguration.launch.name})` : name; - } else { - this.statusBarItem.style.display = 'none'; } } } - - public dispose(): void { - super.dispose(); - this.toDispose = dispose(this.toDispose); - } } diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index 04f9c90f3db..30ffc30d939 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -66,10 +66,10 @@ export class DebugViewlet extends ViewContainerViewlet { })); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - DOM.addClass(parent, 'debug-viewlet'); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + DOM.addClass(parent, 'debug-viewlet'); + }); } public focus(): void { @@ -105,7 +105,7 @@ export class DebugViewlet extends ViewContainerViewlet { return [this.startAction, this.configureAction, this.toggleReplAction]; } - return DebugActionsWidget.getActions(this.allActions, this.toUnbind, this.debugService, this.keybindingService, this.instantiationService); + return DebugActionsWidget.getActions(this.allActions, this.toDispose, this.debugService, this.keybindingService, this.instantiationService); } public get showInitialDebugActions(): boolean { @@ -226,7 +226,7 @@ export class FocusWatchViewAction extends Action { export class FocusCallStackViewAction extends Action { static readonly ID = 'workbench.debug.action.focusCallStackView'; - static LABEL = nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusCallStackView' }, 'Focus CallStack'); + static LABEL = nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusCallStackView' }, 'Focus Call Stack'); constructor(id: string, label: string, @IViewletService private viewletService: IViewletService diff --git a/src/vs/workbench/parts/debug/browser/linkDetector.ts b/src/vs/workbench/parts/debug/browser/linkDetector.ts index e26b2ba73c0..8846aa76a91 100644 --- a/src/vs/workbench/parts/debug/browser/linkDetector.ts +++ b/src/vs/workbench/parts/debug/browser/linkDetector.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; import * as errors from 'vs/base/common/errors'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -106,6 +106,6 @@ export class LinkDetector { startColumn: column } } - }, group).done(null, errors.onUnexpectedError); + }, group).then(null, errors.onUnexpectedError); } } diff --git a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts new file mode 100644 index 00000000000..1a71a9f3c36 --- /dev/null +++ b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts @@ -0,0 +1,543 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as dom from 'vs/base/browser/dom'; +import { normalize, isAbsolute, sep } from 'vs/base/common/paths'; +import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WorkbenchTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService'; +import { renderViewTree, twistiePixels } from 'vs/workbench/parts/debug/browser/baseDebugView'; +import { IAccessibilityProvider, ITree, IRenderer, IDataSource } from 'vs/base/parts/tree/browser/tree'; +import { IDebugSession, IDebugService, IModel, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/parts/debug/common/debug'; +import { Source } from 'vs/workbench/parts/debug/common/debugSource'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { tildify } from 'vs/base/common/labels'; +import { isWindows } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { ltrim } from 'vs/base/common/strings'; +import { RunOnceScheduler } from 'vs/base/common/async'; + +const SMART = true; + +const $ = dom.$; + +const SESSION_TEMPLATE_ID = 'session'; +const SOURCE_TEMPLATE_ID = 'source'; +const ROOT_FOLDER_TEMPLATE_ID = 'node'; + +class BaseTreeItem { + + private _showedMoreThanOne: boolean; + private _children: { [key: string]: BaseTreeItem; }; + private _source: Source; + + constructor(private _parent: BaseTreeItem, private _label: string) { + this._children = {}; + this._showedMoreThanOne = false; + } + + setSource(session: IDebugSession, source: Source): void { + this._source = source; + } + + createIfNeeded(key: string, factory: (parent: BaseTreeItem, label: string) => T): T { + let child = this._children[key]; + if (!child) { + child = factory(this, key); + this._children[key] = child; + } + return child; + } + + remove(key: string): void { + delete this._children[key]; + } + + getTemplateId(): string { + return SOURCE_TEMPLATE_ID; + } + + // a dynamic ID based on the parent chain; required for reparenting (see #55448) + getId(): string { + const parent = this.getParent(); + return parent ? `${parent.getId()}/${this._label}` : this._label; + } + + // skips intermediate single-child nodes + getParent(): BaseTreeItem { + if (this._parent) { + if (this._parent.isSkipped()) { + return this._parent.getParent(); + } + return this._parent; + } + return undefined; + } + + isSkipped(): boolean { + if (this._parent) { + if (this._parent.oneChild()) { + return true; // skipped if I'm the only child of my parents + } + return false; + } + return true; // roots are never skipped + } + + // skips intermediate single-child nodes + hasChildren(): boolean { + const child = this.oneChild(); + if (child) { + return child.hasChildren(); + } + return Object.keys(this._children).length > 0; + } + + // skips intermediate single-child nodes + getChildren(): TPromise { + const child = this.oneChild(); + if (child) { + return child.getChildren(); + } + const array = Object.keys(this._children).map(key => this._children[key]); + return TPromise.as(array.sort((a, b) => this.compare(a, b))); + } + + // skips intermediate single-child nodes + getLabel(separateRootFolder = true) { + const child = this.oneChild(); + if (child) { + const sep = (this instanceof RootFolderTreeItem && separateRootFolder) ? ' • ' : '/'; + return `${this._label}${sep}${child.getLabel()}`; + } + return this._label; + } + + // skips intermediate single-child nodes + getHoverLabel(): string { + let label = this.getLabel(false); + const parent = this.getParent(); + if (parent) { + const hover = parent.getHoverLabel(); + if (hover) { + return `${hover}/${label}`; + } + } + return label; + } + + // skips intermediate single-child nodes + getSource(): Source { + const child = this.oneChild(); + if (child) { + return child.getSource(); + } + return this._source; + } + + protected compare(a: BaseTreeItem, b: BaseTreeItem): number { + if (a._label && b._label) { + return a._label.localeCompare(b._label); + } + return 0; + } + + private oneChild(): BaseTreeItem { + if (SMART && !this._showedMoreThanOne) { + const keys = Object.keys(this._children); + if (keys.length === 1) { + return this._children[keys[0]]; + } + // if a node had more than one child once, it will never be skipped again + if (keys.length > 1) { + this._showedMoreThanOne = true; + } + } + return undefined; + } +} + +class RootFolderTreeItem extends BaseTreeItem { + + constructor(parent: BaseTreeItem, public folder: IWorkspaceFolder) { + super(parent, folder.name); + } + + getTemplateId(): string { + return ROOT_FOLDER_TEMPLATE_ID; + } +} + +class RootTreeItem extends BaseTreeItem { + + constructor(private _debugModel: IModel, private _environmentService: IEnvironmentService, private _contextService: IWorkspaceContextService) { + super(undefined, 'Root'); + this._debugModel.getSessions().forEach(session => { + this.add(session); + }); + } + + add(session: IDebugSession): SessionTreeItem { + return this.createIfNeeded(session.getId(), () => new SessionTreeItem(this, session, this._environmentService, this._contextService)); + } +} + +class SessionTreeItem extends BaseTreeItem { + + private static URL_REGEXP = /^(https?:\/\/[^/]+)(\/.*)$/; + + private _session: IDebugSession; + private _initialized: boolean; + + constructor(parent: BaseTreeItem, session: IDebugSession, private _environmentService: IEnvironmentService, private rootProvider: IWorkspaceContextService) { + super(parent, session.getName(true)); + this._initialized = false; + this._session = session; + } + + getHoverLabel(): string { + return undefined; + } + + getTemplateId(): string { + return SESSION_TEMPLATE_ID; + } + + hasChildren(): boolean { + return true; + } + + getChildren(): TPromise { + + if (!this._initialized) { + this._initialized = true; + return this._session.getLoadedSources().then(paths => { + paths.forEach(path => this.addPath(path)); + return super.getChildren(); + }); + } + + return super.getChildren(); + } + + protected compare(a: BaseTreeItem, b: BaseTreeItem): number { + const acat = this.category(a); + const bcat = this.category(b); + if (acat !== bcat) { + return acat - bcat; + } + return super.compare(a, b); + } + + private category(item: BaseTreeItem): number { + + // workspace scripts come at the beginning in "folder" order + if (item instanceof RootFolderTreeItem) { + return item.folder.index; + } + + // <...> come at the very end + const l = item.getLabel(); + if (l && /^<.+>$/.test(l)) { + return 1000; + } + + // everything else in between + return 999; + } + + addPath(source: Source): void { + + let folder: IWorkspaceFolder; + let url: string; + + let path = source.raw.path; + + const match = SessionTreeItem.URL_REGEXP.exec(path); + if (match && match.length === 3) { + url = match[1]; + path = decodeURI(match[2]); + } else { + if (isAbsolute(path)) { + const resource = URI.file(path); + + // return early if we can resolve a relative path label from the root folder + folder = this.rootProvider ? this.rootProvider.getWorkspaceFolder(resource) : null; + if (folder) { + // strip off the root folder path + path = normalize(ltrim(resource.path.substr(folder.uri.path.length), sep), true); + const hasMultipleRoots = this.rootProvider.getWorkspace().folders.length > 1; + if (hasMultipleRoots) { + path = '/' + path; + } else { + // don't show root folder + folder = undefined; + } + } else { + // on unix try to tildify absolute paths + path = normalize(path, true); + if (!isWindows) { + path = tildify(path, this._environmentService.userHome); + } + } + } + } + + let x: BaseTreeItem = this; + path.split(/[\/\\]/).forEach((segment, i) => { + if (i === 0 && folder) { + x = x.createIfNeeded(folder.name, parent => new RootFolderTreeItem(parent, folder)); + } else if (i === 0 && url) { + x = x.createIfNeeded(url, parent => new BaseTreeItem(parent, url)); + } else { + x = x.createIfNeeded(segment, parent => new BaseTreeItem(parent, segment)); + } + }); + + x.setSource(this._session, source); + } +} + +export class LoadedScriptsView extends TreeViewsViewletPanel { + + private static readonly MEMENTO = 'loadedscriptsview.memento'; + + private treeContainer: HTMLElement; + private loadedScriptsItemType: IContextKey; + private settings: any; + + constructor( + options: IViewletViewOptions, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IEditorService private editorService: IEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IDebugService private debugService: IDebugService + ) { + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService); + this.settings = options.viewletSettings; + this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); + } + + protected renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-loaded-scripts'); + + this.treeContainer = renderViewTree(container); + + this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, + { + dataSource: new LoadedScriptsDataSource(), + renderer: this.instantiationService.createInstance(LoadedScriptsRenderer), + accessibilityProvider: new LoadedSciptsAccessibilityProvider(), + }, + { + ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"), + twistiePixels + } + ); + + const callstackNavigator = new TreeResourceNavigator(this.tree); + this.disposables.push(callstackNavigator); + this.disposables.push(callstackNavigator.openResource(e => { + + const element = e.element; + + if (element instanceof BaseTreeItem) { + const source = element.getSource(); + if (source && source.available) { + const nullRange = { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 }; + source.openInEditor(this.editorService, nullRange, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned); + } + } + })); + + this.disposables.push(this.tree.onDidChangeFocus(() => { + const focus = this.tree.getFocus(); + if (focus instanceof SessionTreeItem) { + this.loadedScriptsItemType.set('session'); + } else { + this.loadedScriptsItemType.reset(); + } + })); + + let nextRefreshIsRecursive = false; + const refreshScheduler = new RunOnceScheduler(() => { + if (this.tree) { + this.tree.refresh(undefined, nextRefreshIsRecursive); + nextRefreshIsRecursive = false; + } + }, 300); + this.disposables.push(refreshScheduler); + + const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService); + this.tree.setInput(root); + + const registerLoadedSourceListener = (session: IDebugSession) => { + this.disposables.push(session.onDidLoadedSource(event => { + const sessionRoot = root.add(session); + sessionRoot.addPath(event.source); + nextRefreshIsRecursive = true; + refreshScheduler.schedule(); + })); + }; + + this.disposables.push(this.debugService.onDidNewSession(registerLoadedSourceListener)); + this.debugService.getModel().getSessions().forEach(registerLoadedSourceListener); + + this.disposables.push(this.debugService.onDidEndSession(session => { + root.remove(session.getId()); + refreshScheduler.schedule(); + })); + } + + layoutBody(size: number): void { + if (this.treeContainer) { + this.treeContainer.style.height = size + 'px'; + } + super.layoutBody(size); + } + + public shutdown(): void { + this.settings[LoadedScriptsView.MEMENTO] = !this.isExpanded(); + super.shutdown(); + } + + dispose(): void { + this.tree = undefined; + super.dispose(); + } +} + +// A good example of data source, renderers, action providers and accessibilty providers can be found in the callStackView.ts + +class LoadedScriptsDataSource implements IDataSource { + + getId(tree: ITree, element: any): string { + return element.getId(); + } + + hasChildren(tree: ITree, element: any): boolean { + return element.hasChildren(); + } + + getChildren(tree: ITree, element: any): TPromise { + return element.getChildren(); + } + + getParent(tree: ITree, element: any): TPromise { + return TPromise.as(element.getParent()); + } + + shouldAutoexpand?(tree: ITree, element: any): boolean { + return element instanceof RootTreeItem || element instanceof SessionTreeItem; + } +} + +interface ISessionTemplateData { + session: HTMLElement; +} + +interface ISourceTemplateData { + source: HTMLElement; +} + +interface INodeTemplateData { + node: HTMLElement; +} + +class LoadedScriptsRenderer implements IRenderer { + + getHeight(tree: ITree, element: any): number { + return 22; + } + + getTemplateId(tree: ITree, element: any): string { + return element.getTemplateId(); + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { + + if (templateId === SESSION_TEMPLATE_ID) { + let data: ISessionTemplateData = Object.create(null); + data.session = dom.append(container, $('.session')); + return data; + } + + if (templateId === SOURCE_TEMPLATE_ID) { + let data: ISourceTemplateData = Object.create(null); + data.source = dom.append(container, $('.source')); + return data; + } + + let data: INodeTemplateData = Object.create(null); + data.node = dom.append(container, $('.node')); + return data; + } + + renderElement(tree: ITree, element: any, templateId: string, templateData: any): void { + if (templateId === SESSION_TEMPLATE_ID) { + this.renderSession(element, templateData); + } else if (templateId === SOURCE_TEMPLATE_ID) { + this.renderSource(element, templateData); + } else if (templateId === ROOT_FOLDER_TEMPLATE_ID) { + this.renderNode(element, templateData); + } + } + + disposeTemplate(tree: ITree, templateId: string, templateData: any): void { + // noop + } + + private renderSession(session: SessionTreeItem, data: ISessionTemplateData): void { + data.session.title = nls.localize('loadedScriptsSession', "Session"); + data.session.textContent = session.getLabel(); + } + + private renderSource(source: BaseTreeItem, data: ISourceTemplateData): void { + data.source.title = source.getHoverLabel(); + data.source.textContent = source.getLabel(); + } + + private renderNode(node: BaseTreeItem, data: INodeTemplateData): void { + data.node.title = node.getHoverLabel(); + data.node.textContent = node.getLabel(); + } +} + +class LoadedSciptsAccessibilityProvider implements IAccessibilityProvider { + + public getAriaLabel(tree: ITree, element: any): string { + + if (element instanceof RootFolderTreeItem) { + return nls.localize('loadedScriptsRootFolderAriaLabel', "Workspace folder {0}, loaded script, debug", element.getLabel()); + } + + if (element instanceof SessionTreeItem) { + return nls.localize('loadedScriptsSessionAriaLabel', "Session {0}, loaded script, debug", element.getLabel()); + } + + if (element instanceof BaseTreeItem) { + if (element.hasChildren()) { + return nls.localize('loadedScriptsFolderAriaLabel', "Folder {0}, loaded script, debug", element.getLabel()); + } else { + return nls.localize('loadedScriptsSourceAriaLabel', "{0}, loaded script, debug", element.getLabel()); + } + } + return null; + } +} 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 6637cb84e53..0785081abcb 100644 --- a/src/vs/workbench/parts/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/parts/debug/browser/media/debug.contribution.css @@ -53,7 +53,7 @@ } .debug-breakpoint, -.monaco-editor .debug-breakpoint-column.debug-breakpoint-column::before { +.monaco-editor .debug-breakpoint-column::before { background: url('breakpoint.svg') center center no-repeat; } @@ -90,11 +90,13 @@ background: url('breakpoint-log.svg') center center no-repeat; } -.debug-breakpoint-log-disabled { +.debug-breakpoint-log-disabled, +.monaco-editor .debug-breakpoint-log-disabled-column::before { background: url('breakpoint-log-disabled.svg') center center no-repeat; } -.debug-breakpoint-log-unverified { +.debug-breakpoint-log-unverified, +.monaco-editor .debug-breakpoint-log-unverified-column::before { background: url('breakpoint-log-unverified.svg') center center no-repeat; } diff --git a/src/vs/workbench/parts/debug/browser/media/debugActionsWidget.css b/src/vs/workbench/parts/debug/browser/media/debugActionsWidget.css index 49fe1168cbc..aa51f17126a 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugActionsWidget.css +++ b/src/vs/workbench/parts/debug/browser/media/debugActionsWidget.css @@ -13,12 +13,12 @@ padding-left: 7px; } -.monaco-workbench .debug-actions-widget .monaco-action-bar .action-item.select-container { - margin-right: 7px; +.monaco-workbench .debug-actions-widget .monaco-action-bar .action-item { + height: 32px; } -.monaco-workbench .debug-actions-widget .monaco-action-bar .action-item .monaco-select-box { - margin-top: 6px; +.monaco-workbench .debug-actions-widget .monaco-action-bar .action-item.select-container { + margin-right: 7px; } .monaco-workbench .debug-actions-widget .drag-area { diff --git a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css index ea156779fae..48ab4d9099a 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css @@ -109,6 +109,7 @@ font-size: 0.9em; padding: 0 3px; margin-left: 0.8em; + line-height: 20px; } .debug-viewlet .monaco-tree .monaco-tree-row.selected .line-number, @@ -350,6 +351,10 @@ /* Breakpoints */ +.debug-viewlet .monaco-list-row { + line-height: 22px; +} + .debug-viewlet .debug-breakpoints .monaco-list-row .breakpoint { padding-left: 2px; } @@ -372,6 +377,7 @@ .debug-viewlet .debug-breakpoints .breakpoint > .icon { width: 19px; height: 19px; + min-width: 19px; } .debug-viewlet .debug-breakpoints .breakpoint > .file-path { @@ -383,6 +389,11 @@ overflow: hidden; } +.debug-viewlet .debug-breakpoints .breakpoint .name { + overflow: hidden; + text-overflow: ellipsis +} + .debug-viewlet .debug-action.remove { background: url('remove.svg') center center no-repeat; } diff --git a/src/vs/workbench/parts/debug/browser/media/repl.css b/src/vs/workbench/parts/debug/browser/media/repl.css index aeac80ce280..639dec32635 100644 --- a/src/vs/workbench/parts/debug/browser/media/repl.css +++ b/src/vs/workbench/parts/debug/browser/media/repl.css @@ -135,7 +135,11 @@ background: url('clear-repl-inverse.svg') center center no-repeat; } -/* Output coloring */ +/* Output coloring and styling */ + +.monaco-workbench .repl .repl-tree .monaco-tree .monaco-tree-row > .content > .output.expression > .ignore { + font-style: italic; +} .vs .monaco-workbench .repl .repl-tree .monaco-tree .monaco-tree-row > .content > .output.expression > .warn { color: #cd9731; @@ -224,4 +228,4 @@ .monaco-workbench .repl .repl-tree .monaco-tree .monaco-tree-row > .content > .output.expression a { text-decoration: underline; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/debug/browser/media/restart-inverse.svg b/src/vs/workbench/parts/debug/browser/media/restart-inverse.svg index 5cda703e42e..eb8116ac100 100644 --- a/src/vs/workbench/parts/debug/browser/media/restart-inverse.svg +++ b/src/vs/workbench/parts/debug/browser/media/restart-inverse.svg @@ -1 +1 @@ -restart \ No newline at end of file +restart \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/browser/media/restart.svg b/src/vs/workbench/parts/debug/browser/media/restart.svg index 1f6f664acff..64971fd7870 100644 --- a/src/vs/workbench/parts/debug/browser/media/restart.svg +++ b/src/vs/workbench/parts/debug/browser/media/restart.svg @@ -1 +1 @@ -restart \ No newline at end of file +restart \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts index ed2b380111e..afb16080fcd 100644 --- a/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/parts/debug/browser/statusbarColorProvider.ts @@ -49,8 +49,8 @@ export class StatusBarColorProvider extends Themable implements IWorkbenchContri } private registerListeners(): void { - this.toUnbind.push(this.debugService.onDidChangeState(state => this.updateStyles())); - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(state => this.updateStyles())); + this._register(this.debugService.onDidChangeState(state => this.updateStyles())); + this._register(this.contextService.onDidChangeWorkbenchState(state => this.updateStyles())); } protected updateStyles(): void { diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index ca2d0ad7dca..eec024ae381 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import severity from 'vs/base/common/severity'; import { Event } from 'vs/base/common/event'; @@ -17,12 +17,15 @@ import { Position } from 'vs/editor/common/core/position'; import { ISuggestion } from 'vs/editor/common/modes'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { Range, IRange } from 'vs/editor/common/core/range'; -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; +import { TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; export const VIEWLET_ID = 'workbench.view.debug'; export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); @@ -30,17 +33,15 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as('debugType', undefined); -export const CONTEXT_DEBUG_STATE = new RawContextKey('debugState', undefined); +export const CONTEXT_DEBUG_STATE = new RawContextKey('debugState', 'inactive'); export const CONTEXT_IN_DEBUG_MODE = new RawContextKey('inDebugMode', false); -export const CONTEXT_NOT_IN_DEBUG_MODE: ContextKeyExpr = CONTEXT_IN_DEBUG_MODE.toNegated(); +export const CONTEXT_NOT_IN_DEBUG_MODE = CONTEXT_IN_DEBUG_MODE.toNegated(); export const CONTEXT_IN_DEBUG_REPL = new RawContextKey('inDebugRepl', false); -export const CONTEXT_NOT_IN_DEBUG_REPL: ContextKeyExpr = CONTEXT_IN_DEBUG_REPL.toNegated(); -export const CONTEXT_ON_FIRST_DEBUG_REPL_LINE = new RawContextKey('onFirstDebugReplLine', false); -export const CONTEXT_ON_LAST_DEBUG_REPL_LINE = new RawContextKey('onLastDebugReplLine', false); export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('breakpointWidgetVisible', false); export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); @@ -49,13 +50,15 @@ export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFo export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false); export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey('breakpointSelected', false); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined); +export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false); +export const CONTEXT_LOADED_SCRIPTS_ITEM_TYPE = new RawContextKey('loadedScriptsItemType', undefined); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const DEBUG_SCHEME = 'debug'; export const INTERNAL_CONSOLE_OPTIONS_SCHEMA = { enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'], default: 'openOnFirstSessionStart', - description: nls.localize('internalConsoleOptions', "Controls behavior of the internal debug console.") + description: nls.localize('internalConsoleOptions', "Controls when the internal debug console should open.") }; // raw @@ -107,51 +110,95 @@ export interface IExpression extends IReplElement, IExpressionContainer { readonly type?: string; } -export interface IRawSession { - readonly root: IWorkspaceFolder; - stackTrace(args: DebugProtocol.StackTraceArguments): TPromise; - exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): TPromise; - scopes(args: DebugProtocol.ScopesArguments): TPromise; - variables(args: DebugProtocol.VariablesArguments): TPromise; - evaluate(args: DebugProtocol.EvaluateArguments): TPromise; - - readonly capabilities: DebugProtocol.Capabilities; - disconnect(restart?: boolean, force?: boolean): TPromise; - custom(request: string, args: any): TPromise; - onDidEvent: Event; - onDidInitialize: Event; - onDidExitAdapter: Event<{ sessionId: string }>; - restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise; - - next(args: DebugProtocol.NextArguments): TPromise; - stepIn(args: DebugProtocol.StepInArguments): TPromise; - stepOut(args: DebugProtocol.StepOutArguments): TPromise; - continue(args: DebugProtocol.ContinueArguments): TPromise; - pause(args: DebugProtocol.PauseArguments): TPromise; - terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise; - stepBack(args: DebugProtocol.StepBackArguments): TPromise; - reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise; - - completions(args: DebugProtocol.CompletionsArguments): TPromise; - setVariable(args: DebugProtocol.SetVariableArguments): TPromise; - source(args: DebugProtocol.SourceArguments): TPromise; +export interface IDebugger { + createDebugAdapter(root: IWorkspaceFolder, outputService: IOutputService, debugPort?: number): TPromise; + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise; + getCustomTelemetryService(): TPromise; } -export enum SessionState { - ATTACH, - LAUNCH +export type ActualBreakpoints = { [id: string]: DebugProtocol.Breakpoint }; + +export enum State { + Inactive, + Initializing, + Stopped, + Running } -export interface ISession extends ITreeElement { - getName(includeRoot: boolean): string; +export class AdapterEndEvent { + error?: Error; + sessionLengthInSeconds: number; + emittedStopped: boolean; +} + +export interface LoadedSourceEvent { + reason: string; + source: Source; +} + +export interface IDebugSession extends ITreeElement, IDisposable { + readonly configuration: IConfig; - readonly raw: IRawSession; - readonly state: SessionState; + readonly unresolvedConfiguration: IConfig; + readonly state: State; + readonly root: IWorkspaceFolder; + + getName(includeRoot: boolean): string; + getSourceForUri(modelUri: uri): Source; + getSource(raw: DebugProtocol.Source): Source; + + rawUpdate(data: IRawModelUpdate): void; + + // session events + onDidEndAdapter: Event; + onDidChangeState: Event; + + // DA capabilities + readonly capabilities: DebugProtocol.Capabilities; + + // DAP events + + onDidLoadedSource: Event; + onDidCustomEvent: Event; + + // DAP request + + initialize(dbgr: IDebugger): TPromise; + launchOrAttach(config: IConfig): TPromise; + restart(): TPromise; + terminate(restart?: boolean /* false */): TPromise; + disconnect(restart?: boolean /* false */): TPromise; + + sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): TPromise; + sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): TPromise; + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise; + + stackTrace(threadId: number, startFrame: number, levels: number): TPromise; + exceptionInfo(threadId: number): TPromise; + scopes(frameId: number): TPromise; + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise; + evaluate(expression: string, frameId?: number, context?: string): TPromise; + customRequest(request: string, args: any): TPromise; + + restartFrame(frameId: number, threadId: number): TPromise; + next(threadId: number): TPromise; + stepIn(threadId: number): TPromise; + stepOut(threadId: number): TPromise; + stepBack(threadId: number): TPromise; + continue(threadId: number): TPromise; + reverseContinue(threadId: number): TPromise; + pause(threadId: number): TPromise; + terminateThreads(threadIds: number[]): TPromise; + + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise; + setVariable(variablesReference: number, name: string, value: string): TPromise; + loadSource(resource: uri): TPromise; + getLoadedSources(): TPromise; + getThread(threadId: number): IThread; getAllThreads(): ReadonlyArray; - getSource(raw: DebugProtocol.Source): Source; - completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise; + clearThreads(removeThreads: boolean, reference?: number): void; } export interface IThread extends ITreeElement { @@ -159,7 +206,7 @@ export interface IThread extends ITreeElement { /** * Process the thread belongs to */ - readonly session: ISession; + readonly session: IDebugSession; /** * Id of the thread generated by the debug adapter backend. @@ -291,7 +338,7 @@ export interface IViewModel extends ITreeElement { /** * Returns the focused debug session or null if no session is stopped. */ - readonly focusedSession: ISession; + readonly focusedSession: IDebugSession; /** * Returns the focused thread or null if no thread is stopped. @@ -310,13 +357,13 @@ export interface IViewModel extends ITreeElement { isMultiSessionView(): boolean; - onDidFocusSession: Event; + onDidFocusSession: Event; onDidFocusStackFrame: Event<{ stackFrame: IStackFrame, explicit: boolean }>; onDidSelectExpression: Event; } export interface IModel extends ITreeElement { - getSessions(): ReadonlyArray; + getSessions(): ReadonlyArray; getBreakpoints(filter?: { uri?: uri, lineNumber?: number, column?: number, enabledOnly?: boolean }): ReadonlyArray; areBreakpointsActivated(): boolean; getFunctionBreakpoints(): ReadonlyArray; @@ -340,15 +387,6 @@ export interface IBreakpointsChangeEvent { sessionOnly?: boolean; } -// Debug enums - -export enum State { - Inactive, - Initializing, - Stopped, - Running -} - // Debug configuration interfaces export interface IDebugConfiguration { @@ -356,7 +394,6 @@ export interface IDebugConfiguration { openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; inlineValues: boolean; - hideActionBar: boolean; toolBarLocation: 'floating' | 'docked' | 'hidden'; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; @@ -372,8 +409,8 @@ export interface IGlobalConfig { export interface IEnvConfig { internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; - preLaunchTask?: string; - postDebugTask?: string; + preLaunchTask?: string | TaskIdentifier; + postDebugTask?: string | TaskIdentifier; debugServer?: number; noDebug?: boolean; } @@ -393,6 +430,7 @@ export interface IConfig extends IEnvConfig { // internals __sessionId?: string; __restart?: any; + __autoAttach?: boolean; port?: number; // TODO } @@ -409,7 +447,7 @@ export interface IDebugAdapter extends IDisposable { startSession(): TPromise; sendMessage(message: DebugProtocol.ProtocolMessage): void; sendResponse(response: DebugProtocol.Response): void; - sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void; + sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timemout?: number): void; stopSession(): TPromise; } @@ -562,17 +600,13 @@ export interface ILaunch { /** * Opens the launch.json file. Creates if it does not exist. */ - openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }>; + openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }>; } // Debug service interfaces export const IDebugService = createDecorator(DEBUG_SERVICE_ID); -export interface DebugEvent extends DebugProtocol.Event { - sessionId?: string; -} - export interface IDebugService { _serviceBrand: any; @@ -589,17 +623,17 @@ export interface IDebugService { /** * Allows to register on new session events. */ - onDidNewSession: Event; + onDidNewSession: Event; + + /** + * Allows to register on sessions about to be created (not yet fully initialised) + */ + onWillNewSession: Event; /** * Allows to register on end session events. */ - onDidEndSession: Event; - - /** - * Allows to register on custom DAP events. - */ - onDidCustomEvent: Event; + onDidEndSession: Event; /** * Gets the current configuration manager. @@ -609,7 +643,7 @@ export interface IDebugService { /** * Sets the focused stack frame and evaluates all expressions against the newly focused stack frame, */ - focusStackFrame(focusedStackFrame: IStackFrame, thread?: IThread, session?: ISession, explicit?: boolean): void; + focusStackFrame(focusedStackFrame: IStackFrame, thread?: IThread, session?: IDebugSession, explicit?: boolean): void; /** * Adds new breakpoints to the model for the file specified with the uri. Notifies debug adapter of breakpoint changes. @@ -656,6 +690,12 @@ export interface IDebugService { */ removeFunctionBreakpoints(id?: string): TPromise; + /** + * Sends all breakpoints to the passed session. + * If session is not passed, sends all breakpoints to each session. + */ + sendAllBreakpoints(session?: IDebugSession): TPromise; + /** * Adds a new expression to the repl. */ @@ -669,7 +709,7 @@ export interface IDebugService { /** * Appends the passed string to the debug repl. */ - logToRepl(value: string, sev?: severity): void; + logToRepl(value: string | IExpression, sev?: severity, source?: IReplElementSource): void; /** * Adds a new watch expression and evaluates it against the debug adapter. @@ -701,18 +741,23 @@ export interface IDebugService { /** * Restarts a session or creates a new one if there is no active session. */ - restartSession(session: ISession): TPromise; + restartSession(session: IDebugSession, restartData?: any): TPromise; /** * Stops the session. If the session does not exist then stops all sessions. */ - stopSession(session: ISession): TPromise; + stopSession(session: IDebugSession): TPromise; /** * Makes unavailable all sources with the passed uri. Source will appear as grayed out in callstack view. */ sourceIsNotAvailable(uri: uri): void; + /** + * returns Session with the given ID (or undefined if ID is not found) + */ + getSession(sessionId: string): IDebugSession; + /** * Gets the current debug model. */ @@ -722,10 +767,15 @@ export interface IDebugService { * Gets the current view model. */ getViewModel(): IViewModel; + + /** + * Try to auto focus the top stack frame of the passed thread. + */ + tryToAutoFocusStackFrame(thread: IThread): TPromise; } // Editor interfaces -export enum BreakpointWidgetContext { +export const enum BreakpointWidgetContext { CONDITION = 0, HIT_COUNT = 1, LOG_MESSAGE = 2 diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 6e8edd9f7cf..ec1f0121bd1 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -4,29 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { TPromise } from 'vs/base/common/winjs.base'; import * as lifecycle from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; -import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; import severity from 'vs/base/common/severity'; import { isObject, isString, isUndefinedOrNull } from 'vs/base/common/types'; import { distinct } from 'vs/base/common/arrays'; import { Range, IRange } from 'vs/editor/common/core/range'; -import { ISuggestion } from 'vs/editor/common/modes'; -import { Position } from 'vs/editor/common/core/position'; import { - ITreeElement, IExpression, IExpressionContainer, ISession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IModel, IReplElementSource, - IConfig, IRawSession, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, SessionState, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint + ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IModel, IReplElementSource, + IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IReplElement, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; -import { mixin } from 'vs/base/common/objects'; import { commonSuffixLength } from 'vs/base/common/strings'; import { sep } from 'vs/base/common/paths'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; const MAX_REPL_LENGTH = 10000; @@ -115,7 +112,7 @@ export class ExpressionContainer implements IExpressionContainer { protected children: TPromise; constructor( - protected session: ISession, + protected session: IDebugSession, private _reference: number, private id: string, public namedVariables = 0, @@ -188,16 +185,12 @@ export class ExpressionContainer implements IExpressionContainer { } private fetchVariables(start: number, count: number, filter: 'indexed' | 'named'): TPromise { - return this.session.raw.variables({ - variablesReference: this.reference, - start, - count, - filter - }).then(response => { - return response && response.body && response.body.variables ? distinct(response.body.variables.filter(v => !!v && isString(v.name)), v => v.name).map( - v => new Variable(this.session, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type) - ) : []; - }, (e: Error) => [new Variable(this.session, this, 0, null, e.message, '', 0, 0, { kind: 'virtual' }, null, false)]); + return this.session.variables(this.reference, filter, start, count).then(response => { + return response && response.body && response.body.variables + ? distinct(response.body.variables.filter(v => !!v && isString(v.name)), v => v.name).map( + v => new Variable(this.session, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type)) + : []; + }, (e: Error) => [new Variable(this.session, this, 0, e.message, e.message, '', 0, 0, { kind: 'virtual' }, null, false)]); } // The adapter explicitly sents the children count of an expression only if there are lots of children which should be chunked. @@ -233,7 +226,7 @@ export class Expression extends ExpressionContainer implements IExpression { } } - public evaluate(session: ISession, stackFrame: IStackFrame, context: string): TPromise { + public evaluate(session: IDebugSession, stackFrame: IStackFrame, context: string): TPromise { if (!session || (!stackFrame && context !== 'repl')) { this.value = context === 'repl' ? nls.localize('startDebugFirst', "Please start a debug session to evaluate") : Expression.DEFAULT_VALUE; this.available = false; @@ -243,11 +236,7 @@ export class Expression extends ExpressionContainer implements IExpression { } this.session = session; - return session.raw.evaluate({ - expression: this.name, - frameId: stackFrame ? stackFrame.frameId : undefined, - context - }).then(response => { + return session.evaluate(this.name, stackFrame ? stackFrame.frameId : undefined, context).then(response => { this.available = !!(response && response.body); if (response && response.body) { this.value = response.body.result; @@ -274,7 +263,7 @@ export class Variable extends ExpressionContainer implements IExpression { public errorMessage: string; constructor( - session: ISession, + session: IDebugSession, public parent: IExpressionContainer, reference: number, public name: string, @@ -292,11 +281,7 @@ export class Variable extends ExpressionContainer implements IExpression { } public setVariable(value: string): TPromise { - return this.session.raw.setVariable({ - name: this.name, - value, - variablesReference: (this.parent).reference - }).then(response => { + return this.session.setVariable((this.parent).reference, name, value).then(response => { if (response && response.body) { this.value = response.body.value; this.type = response.body.type || this.type; @@ -352,7 +337,7 @@ export class StackFrame implements IStackFrame { public getScopes(): TPromise { if (!this.scopes) { - this.scopes = this.thread.session.raw.scopes({ frameId: this.frameId }).then(response => { + this.scopes = this.thread.session.scopes(this.frameId).then(response => { return response && response.body && response.body.scopes ? response.body.scopes.map((rs, index) => new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : null)) : []; @@ -393,7 +378,7 @@ export class StackFrame implements IStackFrame { } public restart(): TPromise { - return this.thread.session.raw.restartFrame({ frameId: this.frameId }, this.thread.threadId); + return this.thread.session.restartFrame(this.frameId, this.thread.threadId); } public toString(): string { @@ -412,7 +397,7 @@ export class Thread implements IThread { public stoppedDetails: IRawStoppedDetails; public stopped: boolean; - constructor(public session: ISession, public name: string, public threadId: number) { + constructor(public session: IDebugSession, public name: string, public threadId: number) { this.stoppedDetails = null; this.callStack = []; this.staleCallStack = []; @@ -461,7 +446,7 @@ export class Thread implements IThread { } private getCallStackImpl(startFrame: number, levels: number): TPromise { - return this.session.raw.stackTrace({ threadId: this.threadId, startFrame, levels }).then(response => { + return this.session.stackTrace(this.threadId, startFrame, levels).then(response => { if (!response || !response.body) { return []; } @@ -493,223 +478,48 @@ export class Thread implements IThread { * Returns exception info promise if the exception was thrown, otherwise null */ public get exceptionInfo(): TPromise { - const session = this.session.raw; if (this.stoppedDetails && this.stoppedDetails.reason === 'exception') { - if (!session.capabilities.supportsExceptionInfoRequest) { - return TPromise.as({ - description: this.stoppedDetails.text, - breakMode: null - }); + if (this.session.capabilities.supportsExceptionInfoRequest) { + return this.session.exceptionInfo(this.threadId); } - - return session.exceptionInfo({ threadId: this.threadId }).then(exception => { - if (!exception) { - return null; - } - - return { - id: exception.body.exceptionId, - description: exception.body.description, - breakMode: exception.body.breakMode, - details: exception.body.details - }; + return TPromise.as({ + description: this.stoppedDetails.text, + breakMode: null }); } - return TPromise.as(null); } public next(): TPromise { - return this.session.raw.next({ threadId: this.threadId }); + return this.session.next(this.threadId); } public stepIn(): TPromise { - return this.session.raw.stepIn({ threadId: this.threadId }); + return this.session.stepIn(this.threadId); } public stepOut(): TPromise { - return this.session.raw.stepOut({ threadId: this.threadId }); + return this.session.stepOut(this.threadId); } public stepBack(): TPromise { - return this.session.raw.stepBack({ threadId: this.threadId }); + return this.session.stepBack(this.threadId); } public continue(): TPromise { - return this.session.raw.continue({ threadId: this.threadId }); + return this.session.continue(this.threadId); } public pause(): TPromise { - return this.session.raw.pause({ threadId: this.threadId }); + return this.session.pause(this.threadId); } public terminate(): TPromise { - return this.session.raw.terminateThreads({ threadIds: [this.threadId] }); + return this.session.terminateThreads([this.threadId]); } public reverseContinue(): TPromise { - return this.session.raw.reverseContinue({ threadId: this.threadId }); - } -} - -export class Session implements ISession { - - private sources: Map; - private threads: Map; - - constructor(private _configuration: { resolved: IConfig, unresolved: IConfig }, private session: IRawSession & ITreeElement) { - this.threads = new Map(); - this.sources = new Map(); - } - - public get configuration(): IConfig { - return this._configuration.resolved; - } - - public get unresolvedConfiguration(): IConfig { - return this._configuration.unresolved; - } - - public get raw(): IRawSession & ITreeElement { - return this.session; - } - - public set raw(value: IRawSession & ITreeElement) { - this.session = value; - } - - public getName(includeRoot: boolean): string { - return includeRoot && this.raw.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.raw.root.uri)})` : this.configuration.name; - } - - public get state(): SessionState { - return this.configuration.type === 'attach' ? SessionState.ATTACH : SessionState.LAUNCH; - } - - public getSourceForUri(modelUri: uri): Source { - return this.sources.get(modelUri.toString()); - } - - public getSource(raw: DebugProtocol.Source): Source { - let source = new Source(raw, this.getId()); - if (this.sources.has(source.uri.toString())) { - source = this.sources.get(source.uri.toString()); - source.raw = mixin(source.raw, raw); - if (source.raw && raw) { - // Always take the latest presentation hint from adapter #42139 - source.raw.presentationHint = raw.presentationHint; - } - } else { - this.sources.set(source.uri.toString(), source); - } - - return source; - } - - public getThread(threadId: number): Thread { - return this.threads.get(threadId); - } - - public getAllThreads(): IThread[] { - const result: IThread[] = []; - this.threads.forEach(t => result.push(t)); - return result; - } - - public getId(): string { - return this.session.getId(); - } - - public rawUpdate(data: IRawModelUpdate): void { - - if (data.thread && !this.threads.has(data.threadId)) { - // A new thread came in, initialize it. - this.threads.set(data.threadId, new Thread(this, data.thread.name, data.thread.id)); - } else if (data.thread && data.thread.name) { - // Just the thread name got updated #18244 - this.threads.get(data.threadId).name = data.thread.name; - } - - if (data.stoppedDetails) { - // Set the availability of the threads' callstacks depending on - // whether the thread is stopped or not - if (data.stoppedDetails.allThreadsStopped) { - this.threads.forEach(thread => { - thread.stoppedDetails = thread.threadId === data.threadId ? data.stoppedDetails : { reason: undefined }; - thread.stopped = true; - thread.clearCallStack(); - }); - } else if (this.threads.has(data.threadId)) { - // One thread is stopped, only update that thread. - const thread = this.threads.get(data.threadId); - thread.stoppedDetails = data.stoppedDetails; - thread.clearCallStack(); - thread.stopped = true; - } - } - } - - public clearThreads(removeThreads: boolean, reference: number = undefined): void { - if (reference !== undefined && reference !== null) { - if (this.threads.has(reference)) { - const thread = this.threads.get(reference); - thread.clearCallStack(); - thread.stoppedDetails = undefined; - thread.stopped = false; - - if (removeThreads) { - this.threads.delete(reference); - } - } - } else { - this.threads.forEach(thread => { - thread.clearCallStack(); - thread.stoppedDetails = undefined; - thread.stopped = false; - }); - - if (removeThreads) { - this.threads.clear(); - ExpressionContainer.allValues.clear(); - } - } - } - - public completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { - if (!this.raw.capabilities.supportsCompletionsRequest) { - return TPromise.as([]); - } - - return this.raw.completions({ - frameId, - text, - column: position.column, - line: position.lineNumber - }).then(response => { - const result: ISuggestion[] = []; - if (response && response.body && response.body.targets) { - response.body.targets.forEach(item => { - if (item && item.label) { - result.push({ - label: item.label, - insertText: item.text || item.label, - type: item.type, - filterText: item.start && item.length && text.substr(item.start, item.length).concat(item.label), - overwriteBefore: item.length || overwriteBefore - }); - } - }); - } - - return result; - }, () => []); - } - - setNotAvailable(modelUri: uri) { - const source = this.sources.get(modelUri.toString()); - if (source) { - source.available = false; - } + return this.session.reverseContinue(this.threadId); } } @@ -786,6 +596,7 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { hitCondition: string, logMessage: string, private _adapterData: any, + private textFileService: ITextFileService, id = generateUuid() ) { super(enabled, hitCondition, condition, logMessage, id); @@ -793,7 +604,16 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { public get lineNumber(): number { const data = this.getSessionData(); - return data && typeof data.line === 'number' ? data.line : this._lineNumber; + return this.verified && data && typeof data.line === 'number' ? data.line : this._lineNumber; + } + + public get verified(): boolean { + const data = this.getSessionData(); + if (data) { + return data.verified && !this.textFileService.isDirty(this.uri); + } + + return true; } public get column(): number { @@ -804,7 +624,14 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { public get message(): string { const data = this.getSessionData(); - return data ? data.message : undefined; + if (!data) { + return undefined; + } + if (this.textFileService.isDirty(this.uri)) { + return nls.localize('breakpointDirtydHover', "Unverified breakpoint. File is modified, please restart debug session."); + } + + return data.message; } public get adapterData(): any { @@ -859,7 +686,8 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak hitCondition: string, condition: string, logMessage: string, - id = generateUuid()) { + id = generateUuid() + ) { super(enabled, hitCondition, condition, logMessage, id); } @@ -897,7 +725,7 @@ export class ThreadAndSessionIds implements ITreeElement { export class Model implements IModel { - private sessions: Session[]; + private sessions: IDebugSession[]; private toDispose: lifecycle.IDisposable[]; private replElements: IReplElement[]; private schedulers = new Map(); @@ -912,7 +740,8 @@ export class Model implements IModel { private breakpointsActivated: boolean, private functionBreakpoints: FunctionBreakpoint[], private exceptionBreakpoints: ExceptionBreakpoint[], - private watchExpressions: Expression[] + private watchExpressions: Expression[], + private textFileService: ITextFileService ) { this.sessions = []; this.replElements = []; @@ -927,15 +756,12 @@ export class Model implements IModel { return 'root'; } - public getSessions(): Session[] { + public getSessions(): IDebugSession[] { return this.sessions; } - public addSession(configuration: { resolved: IConfig, unresolved: IConfig }, raw: IRawSession & ITreeElement): Session { - const session = new Session(configuration, raw); + public addSession(session: IDebugSession): void { this.sessions.push(session); - - return session; } public removeSession(id: string): void { @@ -979,12 +805,12 @@ export class Model implements IModel { } public fetchCallStack(thread: Thread): TPromise { - if (thread.session.raw.capabilities.supportsDelayedStackTraceLoading) { + if (thread.session.capabilities.supportsDelayedStackTraceLoading) { // For improved performance load the first stack frame and then load the rest async. return thread.fetchCallStack(1).then(() => { if (!this.schedulers.has(thread.getId())) { this.schedulers.set(thread.getId(), new RunOnceScheduler(() => { - thread.fetchCallStack(19).done(() => this._onDidChangeCallStack.fire(), errors.onUnexpectedError); + thread.fetchCallStack(19).then(() => this._onDidChangeCallStack.fire()); }, 420)); } @@ -1053,7 +879,7 @@ export class Model implements IModel { } public addBreakpoints(uri: uri, rawData: IBreakpointData[], fireEvent = true): IBreakpoint[] { - const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled, rawBp.condition, rawBp.hitCondition, rawBp.logMessage, undefined, rawBp.id)); + const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled, rawBp.condition, rawBp.hitCondition, rawBp.logMessage, undefined, this.textFileService, rawBp.id)); newBreakpoints.forEach(bp => bp.setSessionId(this.breakpointsSessionId)); this.breakpoints = this.breakpoints.concat(newBreakpoints); this.breakpointsActivated = true; @@ -1192,7 +1018,7 @@ export class Model implements IModel { return this.replElements; } - public addReplExpression(session: ISession, stackFrame: IStackFrame, name: string): TPromise { + public addReplExpression(session: IDebugSession, stackFrame: IStackFrame, name: string): TPromise { const expression = new Expression(name); this.addReplElements([expression]); return expression.evaluate(session, stackFrame, 'repl') @@ -1269,7 +1095,7 @@ export class Model implements IModel { } public sourceIsNotAvailable(uri: uri): void { - this.sessions.forEach(p => p.setNotAvailable(uri)); + this.sessions.forEach(p => p.getSourceForUri(uri).available = false); this._onDidChangeCallStack.fire(); } diff --git a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts index 62b866aa293..5bec6d838af 100644 --- a/src/vs/workbench/parts/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/parts/debug/common/debugProtocol.d.ts @@ -18,7 +18,7 @@ declare module DebugProtocol { type: string; } - /** A client or server-initiated request. */ + /** A client or debug adapter initiated request. */ export interface Request extends ProtocolMessage { // type: 'request'; /** The command to execute. */ @@ -27,7 +27,7 @@ declare module DebugProtocol { arguments?: any; } - /** Server-initiated event. */ + /** A debug adapter initiated event. */ export interface Event extends ProtocolMessage { // type: 'event'; /** Type of event. */ @@ -36,7 +36,7 @@ declare module DebugProtocol { body?: any; } - /** Response to a request. */ + /** Response for a request. */ export interface Response extends ProtocolMessage { // type: 'response'; /** Sequence number of the corresponding request. */ @@ -51,16 +51,24 @@ declare module DebugProtocol { body?: any; } + /** On error (whenever 'success' is false), the body can provide more details. */ + export interface ErrorResponse extends Response { + body: { + /** An optional, structured error message. */ + error?: Message; + }; + } + /** Event message for 'initialized' event type. This event indicates that the debug adapter is ready to accept configuration requests (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). - A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the InitializeRequest has finished). + A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the 'initialize' request has finished). The sequence of events/requests is as follows: - - adapters sends InitializedEvent (after the InitializeRequest has returned) - - frontend sends zero or more SetBreakpointsRequest - - frontend sends one SetFunctionBreakpointsRequest - - frontend sends a SetExceptionBreakpointsRequest if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not defined or false) + - adapters sends 'initialized' event (after the 'initialize' request has returned) + - frontend sends zero or more 'setBreakpoints' requests + - frontend sends one 'setFunctionBreakpoints' request + - frontend sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not defined or false) - frontend sends other future configuration requests - - frontend sends one ConfigurationDoneRequest to indicate the end of the configuration + - frontend sends one 'configurationDone' request to indicate the end of the configuration. */ export interface InitializedEvent extends Event { // event: 'initialized'; @@ -75,10 +83,10 @@ declare module DebugProtocol { body: { /** The reason for the event. For backward compatibility this string is shown in the UI if the 'description' attribute is missing (but it must not be translated). - Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', etc. + Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', etc. */ reason: string; - /** The full reason for the event, e.g. 'Paused on exception'. This string is shown in the UI as is. */ + /** The full reason for the event, e.g. 'Paused on exception'. This string is shown in the UI as is and must be translated. */ description?: string; /** The thread which was stopped. */ threadId?: number; @@ -86,9 +94,9 @@ declare module DebugProtocol { preserveFocusHint?: boolean; /** Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI. */ text?: string; - /** If allThreadsStopped is true, a debug adapter can announce that all threads have stopped. - * The client should use this information to enable that all threads can be expanded to access their stacktraces. - * If the attribute is missing or false, only the thread with the given threadId can be expanded. + /** If 'allThreadsStopped' is true, a debug adapter can announce that all threads have stopped. + - The client should use this information to enable that all threads can be expanded to access their stacktraces. + - If the attribute is missing or false, only the thread with the given threadId can be expanded. */ allThreadsStopped?: boolean; }; @@ -97,20 +105,20 @@ declare module DebugProtocol { /** Event message for 'continued' event type. The event indicates that the execution of the debuggee has continued. Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. 'launch' or 'continue'. - It is only necessary to send a ContinuedEvent if there was no previous request that implied this. + It is only necessary to send a 'continued' event if there was no previous request that implied this. */ export interface ContinuedEvent extends Event { // event: 'continued'; body: { /** The thread which was continued. */ threadId: number; - /** If allThreadsContinued is true, a debug adapter can announce that all threads have continued. */ + /** If 'allThreadsContinued' is true, a debug adapter can announce that all threads have continued. */ allThreadsContinued?: boolean; }; } /** Event message for 'exited' event type. - The event indicates that the debuggee has exited. + The event indicates that the debuggee has exited and returns its exit code. */ export interface ExitedEvent extends Event { // event: 'exited'; @@ -120,8 +128,8 @@ declare module DebugProtocol { }; } - /** Event message for 'terminated' event types. - The event indicates that debugging of the debuggee has terminated. + /** Event message for 'terminated' event type. + The event indicates that debugging of the debuggee has terminated. This does **not** mean that the debuggee itself has exited. */ export interface TerminatedEvent extends Event { // event: 'terminated'; @@ -160,7 +168,7 @@ declare module DebugProtocol { category?: string; /** The output to report. */ output: string; - /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing variablesReference to the VariablesRequest. */ + /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. */ variablesReference?: number; /** An optional source location where the output was produced. */ source?: Source; @@ -249,8 +257,8 @@ declare module DebugProtocol { }; } - /** runInTerminal request; value of command field is 'runInTerminal'. - With this request a debug adapter can run a command in a terminal. + /** RunInTerminal request; value of command field is 'runInTerminal'. + This request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. */ export interface RunInTerminalRequest extends Request { // command: 'runInTerminal'; @@ -271,7 +279,7 @@ declare module DebugProtocol { env?: { [key: string]: string | null; }; } - /** Response to Initialize request. */ + /** Response to 'runInTerminal' request. */ export interface RunInTerminalResponse extends Response { body: { /** The process ID. */ @@ -279,15 +287,11 @@ declare module DebugProtocol { }; } - /** On error that is whenever 'success' is false, the body can provide more details. */ - export interface ErrorResponse extends Response { - body: { - /** An optional, structured error message. */ - error?: Message; - }; - } - - /** Initialize request; value of command field is 'initialize'. */ + /** Initialize request; value of command field is 'initialize'. + The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. + Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. + The 'initialize' request may only be sent once. + */ export interface InitializeRequest extends Request { // command: 'initialize'; arguments: InitializeRequestArguments; @@ -326,16 +330,14 @@ declare module DebugProtocol { } /** ConfigurationDone request; value of command field is 'configurationDone'. - The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the InitializedEvent). + The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the 'initialized' event). */ export interface ConfigurationDoneRequest extends Request { // command: 'configurationDone'; arguments?: ConfigurationDoneArguments; } - /** Arguments for 'configurationDone' request. - The configurationDone request has no standardized attributes. - */ + /** Arguments for 'configurationDone' request. */ export interface ConfigurationDoneArguments { } @@ -343,7 +345,9 @@ declare module DebugProtocol { export interface ConfigurationDoneResponse extends Response { } - /** Launch request; value of command field is 'launch'. */ + /** Launch request; value of command field is 'launch'. + The launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. + */ export interface LaunchRequest extends Request { // command: 'launch'; arguments: LaunchRequestArguments; @@ -364,7 +368,9 @@ declare module DebugProtocol { export interface LaunchResponse extends Response { } - /** Attach request; value of command field is 'attach'. */ + /** Attach request; value of command field is 'attach'. + The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. + */ export interface AttachRequest extends Request { // command: 'attach'; arguments: AttachRequestArguments; @@ -394,9 +400,7 @@ declare module DebugProtocol { arguments?: RestartArguments; } - /** Arguments for 'restart' request. - The restart request has no standardized attributes. - */ + /** Arguments for 'restart' request. */ export interface RestartArguments { } @@ -404,7 +408,9 @@ declare module DebugProtocol { export interface RestartResponse extends Response { } - /** Disconnect request; value of command field is 'disconnect'. */ + /** Disconnect request; value of command field is 'disconnect'. + The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' (if supported by the debug adapter). + */ export interface DisconnectRequest extends Request { // command: 'disconnect'; arguments?: DisconnectArguments; @@ -412,6 +418,8 @@ declare module DebugProtocol { /** Arguments for 'disconnect' request. */ export interface DisconnectArguments { + /** A value of true indicates that this 'disconnect' request is part of a restart sequence. */ + restart?: boolean; /** Indicates whether the debuggee should be terminated when the debugger is disconnected. If unspecified, the debug adapter is free to do whatever it thinks is best. A client can only rely on this attribute being properly honored if a debug adapter returns true for the 'supportTerminateDebuggee' capability. @@ -423,10 +431,28 @@ declare module DebugProtocol { export interface DisconnectResponse extends Response { } + /** Terminate request; value of command field is 'terminate'. + The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. + */ + export interface TerminateRequest extends Request { + // command: 'terminate'; + arguments?: TerminateArguments; + } + + /** Arguments for 'terminate' request. */ + export interface TerminateArguments { + /** A value of true indicates that this 'terminate' request is part of a restart sequence. */ + restart?: boolean; + } + + /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ + export interface TerminateResponse extends Response { + } + /** SetBreakpoints request; value of command field is 'setBreakpoints'. Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. To clear all breakpoint for a source, specify an empty array. - When a breakpoint is hit, a StoppedEvent (event type 'breakpoint') is generated. + When a breakpoint is hit, a 'stopped' event (with reason 'breakpoint') is generated. */ export interface SetBreakpointsRequest extends Request { // command: 'setBreakpoints'; @@ -435,7 +461,7 @@ declare module DebugProtocol { /** Arguments for 'setBreakpoints' request. */ export interface SetBreakpointsArguments { - /** The source location of the breakpoints; either source.path or source.reference must be specified. */ + /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ source: Source; /** The code locations of the breakpoints. */ breakpoints?: SourceBreakpoint[]; @@ -449,11 +475,11 @@ declare module DebugProtocol { Returned is information about each breakpoint created by this request. This includes the actual code location and whether the breakpoint could be verified. The breakpoints returned are in the same order as the elements of the 'breakpoints' - (or the deprecated 'lines') in the SetBreakpointsArguments. + (or the deprecated 'lines') array in the arguments. */ export interface SetBreakpointsResponse extends Response { body: { - /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') in the SetBreakpointsArguments. */ + /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ breakpoints: Breakpoint[]; }; } @@ -461,7 +487,7 @@ declare module DebugProtocol { /** SetFunctionBreakpoints request; value of command field is 'setFunctionBreakpoints'. Sets multiple function breakpoints and clears all previous function breakpoints. To clear all function breakpoint, specify an empty array. - When a function breakpoint is hit, a StoppedEvent (event type 'function breakpoint') is generated. + When a function breakpoint is hit, a 'stopped' event (event type 'function breakpoint') is generated. */ export interface SetFunctionBreakpointsRequest extends Request { // command: 'setFunctionBreakpoints'; @@ -485,7 +511,7 @@ declare module DebugProtocol { } /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a StoppedEvent is fired (event type 'exception'). + The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). */ export interface SetExceptionBreakpointsRequest extends Request { // command: 'setExceptionBreakpoints'; @@ -514,21 +540,21 @@ declare module DebugProtocol { /** Arguments for 'continue' request. */ export interface ContinueArguments { - /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the allThreadsContinued attribute in the response to true. */ + /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. */ threadId: number; } /** Response to 'continue' request. */ export interface ContinueResponse extends Response { body: { - /** If true, the continue request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ + /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ allThreadsContinued?: boolean; }; } /** Next request; value of command field is 'next'. The request starts the debuggee to run again for one step. - The debug adapter first sends the NextResponse and then a StoppedEvent (event type 'step') after the step has completed. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ export interface NextRequest extends Request { // command: 'next'; @@ -548,7 +574,7 @@ declare module DebugProtocol { /** StepIn request; value of command field is 'stepIn'. The request starts the debuggee to step into a function/method if possible. If it cannot step into a target, 'stepIn' behaves like 'next'. - The debug adapter first sends the StepInResponse and then a StoppedEvent (event type 'step') after the step has completed. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. If there are multiple function/method calls (or other targets) on the source line, the optional argument 'targetId' can be used to control into which target the 'stepIn' should occur. The list of possible targets for a given source line can be retrieved via the 'stepInTargets' request. @@ -572,7 +598,7 @@ declare module DebugProtocol { /** StepOut request; value of command field is 'stepOut'. The request starts the debuggee to run again for one step. - The debug adapter first sends the StepOutResponse and then a StoppedEvent (event type 'step') after the step has completed. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ export interface StepOutRequest extends Request { // command: 'stepOut'; @@ -591,7 +617,7 @@ declare module DebugProtocol { /** StepBack request; value of command field is 'stepBack'. The request starts the debuggee to run one step backwards. - The debug adapter first sends the StepBackResponse and then a StoppedEvent (event type 'step') after the step has completed. Clients should only call this request if the capability supportsStepBack is true. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface StepBackRequest extends Request { // command: 'stepBack'; @@ -609,7 +635,7 @@ declare module DebugProtocol { } /** ReverseContinue request; value of command field is 'reverseContinue'. - The request starts the debuggee to run backward. Clients should only call this request if the capability supportsStepBack is true. + The request starts the debuggee to run backward. Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface ReverseContinueRequest extends Request { // command: 'reverseContinue'; @@ -628,7 +654,7 @@ declare module DebugProtocol { /** RestartFrame request; value of command field is 'restartFrame'. The request restarts execution of the specified stackframe. - The debug adapter first sends the RestartFrameResponse and then a StoppedEvent (event type 'restart') after the restart has completed. + The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. */ export interface RestartFrameRequest extends Request { // command: 'restartFrame'; @@ -649,7 +675,7 @@ declare module DebugProtocol { The request sets the location where the debuggee will continue to run. This makes it possible to skip the execution of code or to executed code again. The code between the current location and the goto target is not executed but skipped. - The debug adapter first sends the GotoResponse and then a StoppedEvent (event type 'goto'). + The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. */ export interface GotoRequest extends Request { // command: 'goto'; @@ -670,7 +696,7 @@ declare module DebugProtocol { /** Pause request; value of command field is 'pause'. The request suspenses the debuggee. - The debug adapter first sends the PauseResponse and then a StoppedEvent (event type 'pause') after the thread has been paused successfully. + The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. */ export interface PauseRequest extends Request { // command: 'pause'; @@ -687,7 +713,9 @@ declare module DebugProtocol { export interface PauseResponse extends Response { } - /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state. */ + /** StackTrace request; value of command field is 'stackTrace'. + The request returns a stacktrace from the current execution state. + */ export interface StackTraceRequest extends Request { // command: 'stackTrace'; arguments: StackTraceArguments; @@ -770,7 +798,7 @@ declare module DebugProtocol { }; } - /** setVariable request; value of command field is 'setVariable'. + /** SetVariable request; value of command field is 'setVariable'. Set the variable with the given name in the variable container to a new value. */ export interface SetVariableRequest extends Request { @@ -836,7 +864,7 @@ declare module DebugProtocol { }; } - /** Thread request; value of command field is 'threads'. + /** Threads request; value of command field is 'threads'. The request retrieves a list of all threads. */ export interface ThreadsRequest extends Request { @@ -851,7 +879,7 @@ declare module DebugProtocol { }; } - /** Terminate thread request; value of command field is 'terminateThreads'. + /** TerminateThreads request; value of command field is 'terminateThreads'. The request terminates the threads with the given ids. */ export interface TerminateThreadsRequest extends Request { @@ -869,7 +897,9 @@ declare module DebugProtocol { export interface TerminateThreadsResponse extends Response { } - /** Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. */ + /** Modules request; value of command field is 'modules'. + Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. + */ export interface ModulesRequest extends Request { // command: 'modules'; arguments: ModulesArguments; @@ -893,15 +923,15 @@ declare module DebugProtocol { }; } - /** Retrieves the set of all sources currently loaded by the debugged process. */ + /** LoadedSources request; value of command field is 'loadedSources'. + Retrieves the set of all sources currently loaded by the debugged process. + */ export interface LoadedSourcesRequest extends Request { // command: 'loadedSources'; arguments?: LoadedSourcesArguments; } - /** Arguments for 'loadedSources' request. - The 'loadedSources' request has no standardized arguments. - */ + /** Arguments for 'loadedSources' request. */ export interface LoadedSourcesArguments { } @@ -1057,7 +1087,7 @@ declare module DebugProtocol { }; } - /** CompletionsRequest request; value of command field is 'completions'. + /** Completions request; value of command field is 'completions'. Returns a list of possible completions for a given caret position and text. The CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true. */ @@ -1086,8 +1116,8 @@ declare module DebugProtocol { }; } - /** ExceptionInfoRequest request; value of command field is 'exceptionInfo'. - Retrieves the details of the exception that caused the StoppedEvent to be raised. + /** ExceptionInfo request; value of command field is 'exceptionInfo'. + Retrieves the details of the exception that caused this event to be raised. */ export interface ExceptionInfoRequest extends Request { // command: 'exceptionInfo'; @@ -1166,6 +1196,8 @@ declare module DebugProtocol { supportsTerminateThreadsRequest?: boolean; /** The debug adapter supports the 'setExpression' request. */ supportsSetExpression?: boolean; + /** The debug adapter supports the 'terminate' request. */ + supportsTerminateRequest?: boolean; } /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ @@ -1269,7 +1301,7 @@ declare module DebugProtocol { export interface Source { /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ name?: string; - /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its vaule is 0). */ + /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ path?: string; /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. */ sourceReference?: number; diff --git a/src/vs/workbench/parts/debug/common/debugSource.ts b/src/vs/workbench/parts/debug/common/debugSource.ts index 71510c78264..d684be1212a 100644 --- a/src/vs/workbench/parts/debug/common/debugSource.ts +++ b/src/vs/workbench/parts/debug/common/debugSource.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { DEBUG_SCHEME } from 'vs/workbench/parts/debug/common/debug'; diff --git a/src/vs/workbench/parts/debug/common/debugUtils.ts b/src/vs/workbench/parts/debug/common/debugUtils.ts index e3c78db313c..5d1f7bf6405 100644 --- a/src/vs/workbench/parts/debug/common/debugUtils.ts +++ b/src/vs/workbench/parts/debug/common/debugUtils.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { IConfig } from 'vs/workbench/parts/debug/common/debug'; + const _formatPIIRegexp = /{([^}]+)}/g; export function formatPII(value: string, excludePII: boolean, args: { [key: string]: string }): string { @@ -17,6 +20,10 @@ export function formatPII(value: string, excludePII: boolean, args: { [key: stri }); } +export function isExtensionHostDebugging(config: IConfig) { + return config.type && equalsIgnoreCase(config.type === 'vslsShare' ? (config).adapterProxy.configuration.type : config.type, 'extensionhost'); +} + export function getExactExpressionStartAndEnd(lineContent: string, looseStart: number, looseEnd: number): { start: number, end: number } { let matchingExpression: string = undefined; let startOffset = 0; diff --git a/src/vs/workbench/parts/debug/common/debugViewModel.ts b/src/vs/workbench/parts/debug/common/debugViewModel.ts index 7c945318b01..55f4be39b3d 100644 --- a/src/vs/workbench/parts/debug/common/debugViewModel.ts +++ b/src/vs/workbench/parts/debug/common/debugViewModel.ts @@ -4,41 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, ISession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED } from 'vs/workbench/parts/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED } from 'vs/workbench/parts/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; export class ViewModel implements IViewModel { + firstSessionStart = true; + private _focusedStackFrame: IStackFrame; - private _focusedSession: ISession; + private _focusedSession: IDebugSession; private _focusedThread: IThread; private selectedExpression: IExpression; private selectedFunctionBreakpoint: IFunctionBreakpoint; - private readonly _onDidFocusSession: Emitter; + private readonly _onDidFocusSession: Emitter; private readonly _onDidFocusStackFrame: Emitter<{ stackFrame: IStackFrame, explicit: boolean }>; private readonly _onDidSelectExpression: Emitter; private multiSessionView: boolean; private expressionSelectedContextKey: IContextKey; private breakpointSelectedContextKey: IContextKey; + private loadedScriptsSupportedContextKey: IContextKey; constructor(contextKeyService: IContextKeyService) { - this._onDidFocusSession = new Emitter(); + this._onDidFocusSession = new Emitter(); this._onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame, explicit: boolean }>(); this._onDidSelectExpression = new Emitter(); this.multiSessionView = false; this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); + this.loadedScriptsSupportedContextKey = CONTEXT_LOADED_SCRIPTS_SUPPORTED.bindTo(contextKeyService); } - public getId(): string { + getId(): string { return 'root'; } - public get focusedSession(): ISession { + get focusedSession(): IDebugSession { return this._focusedSession; } - public get focusedThread(): IThread { + get focusedThread(): IThread { if (this._focusedStackFrame) { return this._focusedStackFrame.thread; } @@ -52,61 +56,63 @@ export class ViewModel implements IViewModel { return undefined; } - public get focusedStackFrame(): IStackFrame { + get focusedStackFrame(): IStackFrame { return this._focusedStackFrame; } - public setFocus(stackFrame: IStackFrame, thread: IThread, session: ISession, explicit: boolean): void { - let shouldEmit = this._focusedSession !== session || this._focusedThread !== thread || this._focusedStackFrame !== stackFrame; + setFocus(stackFrame: IStackFrame, thread: IThread, session: IDebugSession, explicit: boolean): void { + const shouldEmit = this._focusedSession !== session || this._focusedThread !== thread || this._focusedStackFrame !== stackFrame; + this._focusedStackFrame = stackFrame; + this._focusedThread = thread; if (this._focusedSession !== session) { this._focusedSession = session; this._onDidFocusSession.fire(session); } - this._focusedThread = thread; - this._focusedStackFrame = stackFrame; + + this.loadedScriptsSupportedContextKey.set(session && session.capabilities.supportsLoadedSourcesRequest); if (shouldEmit) { this._onDidFocusStackFrame.fire({ stackFrame, explicit }); } } - public get onDidFocusSession(): Event { + get onDidFocusSession(): Event { return this._onDidFocusSession.event; } - public get onDidFocusStackFrame(): Event<{ stackFrame: IStackFrame, explicit: boolean }> { + get onDidFocusStackFrame(): Event<{ stackFrame: IStackFrame, explicit: boolean }> { return this._onDidFocusStackFrame.event; } - public getSelectedExpression(): IExpression { + getSelectedExpression(): IExpression { return this.selectedExpression; } - public setSelectedExpression(expression: IExpression) { + setSelectedExpression(expression: IExpression) { this.selectedExpression = expression; this.expressionSelectedContextKey.set(!!expression); this._onDidSelectExpression.fire(expression); } - public get onDidSelectExpression(): Event { + get onDidSelectExpression(): Event { return this._onDidSelectExpression.event; } - public getSelectedFunctionBreakpoint(): IFunctionBreakpoint { + getSelectedFunctionBreakpoint(): IFunctionBreakpoint { return this.selectedFunctionBreakpoint; } - public setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint): void { + setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint): void { this.selectedFunctionBreakpoint = functionBreakpoint; this.breakpointSelectedContextKey.set(!!functionBreakpoint); } - public isMultiSessionView(): boolean { + isMultiSessionView(): boolean { return this.multiSessionView; } - public setMultiSessionView(isMultiSessionView: boolean): void { + setMultiSessionView(isMultiSessionView: boolean): void { this.multiSessionView = isMultiSessionView; } } diff --git a/src/vs/workbench/parts/debug/common/replHistory.ts b/src/vs/workbench/parts/debug/common/replHistory.ts deleted file mode 100644 index 92beb99600d..00000000000 --- a/src/vs/workbench/parts/debug/common/replHistory.ts +++ /dev/null @@ -1,117 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const MAX_HISTORY_ENTRIES = 50; - -/** - * The repl history has the following characteristics: - * - the history is stored in local storage up to N items - * - every time a expression is evaluated, it is being added to the history - * - when starting to navigate in history, the current expression is remembered to be able to go back - * - when navigating in history and making changes to any expression, these changes are remembered until a expression is evaluated - * - the navigation state is not remembered so that the user always ends up at the end of the history stack when evaluating a expression - */ -export class ReplHistory { - - private historyPointer: number; - private currentExpressionStoredMarkers: boolean; - private historyOverwrites: Map; - - constructor(private history: string[]) { - this.historyPointer = this.history.length; - this.currentExpressionStoredMarkers = false; - this.historyOverwrites = new Map(); - } - - public next(): string { - return this.navigate(false); - } - - public previous(): string { - return this.navigate(true); - } - - private navigate(previous: boolean): string { - // validate new pointer - let newPointer = -1; - if (previous && this.historyPointer > 0 && this.history.length > this.historyPointer - 1) { - newPointer = this.historyPointer - 1; - } else if (!previous && this.history.length > this.historyPointer + 1) { - newPointer = this.historyPointer + 1; - } - - if (newPointer >= 0) { - - // remember pointer for next navigation - this.historyPointer = newPointer; - - // check for overwrite - if (this.historyOverwrites.has(newPointer.toString())) { - return this.historyOverwrites.get(newPointer.toString()); - } - - return this.history[newPointer]; - } - - return null; - } - - public remember(expression: string, fromPrevious: boolean): void { - let previousPointer: number; - - // this method is called after the user has navigated in the history. Therefor we need to - // restore the value of the pointer from the point when the user started the navigation. - if (fromPrevious) { - previousPointer = this.historyPointer + 1; - } else { - previousPointer = this.historyPointer - 1; - } - - // when the user starts to navigate in history, add the current expression to the history - // once so that the user can always navigate back to it and does not loose its data. - if (previousPointer === this.history.length && !this.currentExpressionStoredMarkers) { - this.history.push(expression); - this.currentExpressionStoredMarkers = true; - } - - // keep edits that are made to history items up until the user actually evaluates a expression - else { - this.historyOverwrites.set(previousPointer.toString(), expression); - } - } - - public evaluated(expression: string): void { - // clear current expression that was stored previously to support history navigation now on evaluate - if (this.currentExpressionStoredMarkers) { - this.history.pop(); - } - - // keep in local history if expression provided and not equal to previous expression stored in history - if (expression && (this.history.length === 0 || this.history[this.history.length - 1] !== expression)) { - this.history.push(expression); - } - - // advance History Pointer to the end - this.historyPointer = this.history.length; - - // reset marker - this.currentExpressionStoredMarkers = false; - - // reset overwrites - this.historyOverwrites.clear(); - } - - public save(): string[] { - // remove current expression from history since it was not evaluated - if (this.currentExpressionStoredMarkers) { - this.history.pop(); - } - if (this.history.length > MAX_HISTORY_ENTRIES) { - this.history = this.history.splice(this.history.length - MAX_HISTORY_ENTRIES, MAX_HISTORY_ENTRIES); - } - - return this.history; - } -} diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts index bbf587bfb00..3bcac2b3693 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts @@ -5,7 +5,6 @@ import 'vs/css!../browser/media/breakpointWidget'; import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import * as lifecycle from 'vs/base/common/lifecycle'; @@ -17,24 +16,24 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DEBUG_SCHEME, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, CONTEXT_IN_BREAKPOINT_WIDGET } from 'vs/workbench/parts/debug/common/debug'; import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { SimpleDebugEditor } from 'vs/workbench/parts/debug/electron-browser/simpleDebugEditor'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IModelService } from 'vs/editor/common/services/modelService'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { SuggestRegistry, ISuggestResult, SuggestContext } from 'vs/editor/common/modes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; -import { wireCancellationToken } from 'vs/base/common/async'; import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest'; -import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; const IPrivateBreakpointWidgetService = createDecorator('privateBreakopintWidgetService'); @@ -129,7 +128,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([nls.localize('expression', "Expression"), nls.localize('hitCount', "Hit Count"), nls.localize('logMessage', "Log Message")], this.context, this.contextViewService); + const selectBox = new SelectBox([nls.localize('expression', "Expression"), nls.localize('hitCount', "Hit Count"), nls.localize('logMessage', "Log Message")], this.context, this.contextViewService, null, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); this.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); @@ -183,7 +182,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi condition, hitCondition, logMessage - }]).done(null, errors.onUnexpectedError); + }]); } } @@ -201,11 +200,11 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, scopedContextKeyService], [IPrivateBreakpointWidgetService, this])); - const options = SimpleDebugEditor.getEditorOptions(); - const codeEditorWidgetOptions = SimpleDebugEditor.getCodeEditorWidgetOptions(); + const options = getSimpleEditorOptions(); + const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions(); this.input = scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions); CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); - const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:breakpointinput`), true); + const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true); this.input.setModel(model); this.toDispose.push(model); const setDecorations = () => { @@ -218,9 +217,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(SuggestRegistry.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { provideCompletionItems: (model: ITextModel, position: Position, _context: SuggestContext, token: CancellationToken): Thenable => { - let suggestionsPromise: TPromise; + let suggestionsPromise: Promise; if (this.context === Context.CONDITION || this.context === Context.LOG_MESSAGE && this.isCurlyBracketOpen()) { - suggestionsPromise = provideSuggestionItems(this.editor.getModel(), new Position(this.lineNumber, 1), 'none', undefined, _context).then(suggestions => { + suggestionsPromise = provideSuggestionItems(this.editor.getModel(), new Position(this.lineNumber, 1), 'none', undefined, _context, token).then(suggestions => { let overwriteBefore = 0; if (this.context === Context.CONDITION) { @@ -242,10 +241,10 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi }; }); } else { - suggestionsPromise = TPromise.as({ suggestions: [] }); + suggestionsPromise = Promise.resolve({ suggestions: [] }); } - return wireCancellationToken(token, suggestionsPromise); + return suggestionsPromise; } })); } @@ -298,7 +297,8 @@ class AcceptBreakpointWidgetInputAction extends EditorCommand { precondition: CONTEXT_BREAKPOINT_WIDGET_VISIBLE, kbOpts: { kbExpr: CONTEXT_IN_BREAKPOINT_WIDGET, - primary: KeyCode.Enter + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib } }); } @@ -317,7 +317,8 @@ class CloseBreakpointWidgetCommand extends EditorCommand { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape] + secondary: [KeyMod.Shift | KeyCode.Escape], + weight: KeybindingWeight.EditorContrib } }); } diff --git a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts index 36a064b1969..df72e464cfa 100644 --- a/src/vs/workbench/parts/debug/electron-browser/callStackView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/callStackView.ts @@ -7,10 +7,9 @@ import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as errors from 'vs/base/common/errors'; import { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, ISession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE } from 'vs/workbench/parts/debug/common/debug'; -import { Thread, StackFrame, ThreadAndSessionIds, Session, Model } from 'vs/workbench/parts/debug/common/debugModel'; +import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE } from 'vs/workbench/parts/debug/common/debug'; +import { Thread, StackFrame, ThreadAndSessionIds, Model } from 'vs/workbench/parts/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MenuId } from 'vs/platform/actions/common/actions'; @@ -27,6 +26,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { DebugSession } from 'vs/workbench/parts/debug/electron-browser/debugSession'; const $ = dom.$; @@ -81,15 +82,15 @@ export class CallStackView extends TreeViewsViewletPanel { this.needsRefresh = false; (this.tree.getInput() === newTreeInput ? this.tree.refresh() : this.tree.setInput(newTreeInput)) - .done(() => this.updateTreeSelection(), errors.onUnexpectedError); + .then(() => this.updateTreeSelection()); }, 50); } protected renderHeaderTitle(container: HTMLElement): void { - const title = dom.append(container, $('.title.debug-call-stack-title')); - const name = dom.append(title, $('span')); - name.textContent = this.options.title; - this.pauseMessage = dom.append(title, $('span.pause-message')); + let titleContainer = dom.append(container, $('.debug-call-stack-title')); + super.renderHeaderTitle(titleContainer, this.options.title); + + this.pauseMessage = dom.append(titleContainer, $('span.pause-message')); this.pauseMessage.hidden = true; this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label')); } @@ -120,12 +121,12 @@ export class CallStackView extends TreeViewsViewletPanel { const element = e.element; if (element instanceof StackFrame) { this.debugService.focusStackFrame(element, element.thread, element.thread.session, true); - element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned).done(undefined, errors.onUnexpectedError); + element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned); } if (element instanceof Thread) { this.debugService.focusStackFrame(undefined, element, element.session, true); } - if (element instanceof Session) { + if (element instanceof DebugSession) { this.debugService.focusStackFrame(undefined, undefined, element, true); } if (element instanceof ThreadAndSessionIds) { @@ -133,7 +134,7 @@ export class CallStackView extends TreeViewsViewletPanel { const thread = session && session.getThread(element.threadId); if (thread) { (thread).fetchCallStack() - .done(() => this.tree.refresh(), errors.onUnexpectedError); + .then(() => this.tree.refresh()); } } })); @@ -143,7 +144,7 @@ export class CallStackView extends TreeViewsViewletPanel { this.callStackItemType.set('stackFrame'); } else if (focus instanceof Thread) { this.callStackItemType.set('thread'); - } else if (focus instanceof Session) { + } else if (focus instanceof DebugSession) { this.callStackItemType.set('session'); } else { this.callStackItemType.reset(); @@ -166,7 +167,7 @@ export class CallStackView extends TreeViewsViewletPanel { return; } - this.updateTreeSelection().done(undefined, errors.onUnexpectedError); + this.updateTreeSelection(); })); // Schedule the update of the call stack tree if the viewlet is opened after a session started #14684 @@ -191,7 +192,7 @@ export class CallStackView extends TreeViewsViewletPanel { const stackFrame = this.debugService.getViewModel().focusedStackFrame; const thread = this.debugService.getViewModel().focusedThread; const session = this.debugService.getViewModel().focusedSession; - const updateSelection = (element: IStackFrame | ISession) => { + const updateSelection = (element: IStackFrame | IDebugSession) => { this.ignoreSelectionChangedEvent = true; try { this.tree.setSelection([element]); @@ -270,7 +271,7 @@ class CallStackActionProvider implements IActionProvider { public getSecondaryActions(tree: ITree, element: any): TPromise { const actions: IAction[] = []; - if (element instanceof Session) { + if (element instanceof DebugSession) { actions.push(this.instantiationService.createInstance(RestartAction, RestartAction.ID, RestartAction.LABEL)); actions.push(new StopAction(StopAction.ID, StopAction.LABEL, this.debugService, this.keybindingService)); } else if (element instanceof Thread) { @@ -287,7 +288,7 @@ class CallStackActionProvider implements IActionProvider { actions.push(new Separator()); actions.push(new TerminateThreadAction(TerminateThreadAction.ID, TerminateThreadAction.LABEL, this.debugService, this.keybindingService)); } else if (element instanceof StackFrame) { - if (element.thread.session.raw.capabilities.supportsRestartFrame) { + if (element.thread.session.capabilities.supportsRestartFrame) { actions.push(new RestartFrameAction(RestartFrameAction.ID, RestartFrameAction.LABEL, this.debugService, this.keybindingService)); } actions.push(new CopyStackTraceAction(CopyStackTraceAction.ID, CopyStackTraceAction.LABEL)); @@ -312,7 +313,7 @@ class CallStackDataSource implements IDataSource { } public hasChildren(tree: ITree, element: any): boolean { - return element instanceof Model || element instanceof Session || (element instanceof Thread && (element).stopped); + return element instanceof Model || element instanceof DebugSession || (element instanceof Thread && (element).stopped); } public getChildren(tree: ITree, element: any): TPromise { @@ -323,7 +324,7 @@ class CallStackDataSource implements IDataSource { return TPromise.as(element.getSessions()); } - const session = element; + const session = element; return TPromise.as(session.getAllThreads()); } @@ -335,7 +336,7 @@ class CallStackDataSource implements IDataSource { } return callStackPromise.then(() => { - if (callStack.length === 1 && thread.session.raw.capabilities.supportsDelayedStackTraceLoading) { + if (callStack.length === 1 && thread.session.capabilities.supportsDelayedStackTraceLoading) { // To reduce flashing of the call stack view simply append the stale call stack // once we have the correct data the tree will refresh and we will no longer display it. callStack = callStack.concat(thread.getStaleCallStack().slice(1)); @@ -397,6 +398,7 @@ class CallStackRenderer implements IRenderer { constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, + @ILabelService private labelService: ILabelService ) { // noop } @@ -406,7 +408,7 @@ class CallStackRenderer implements IRenderer { } public getTemplateId(tree: ITree, element: any): string { - if (element instanceof Session) { + if (element instanceof DebugSession) { return CallStackRenderer.SESSION_TEMPLATE_ID; } if (element instanceof Thread) { @@ -476,11 +478,11 @@ class CallStackRenderer implements IRenderer { } else if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) { this.renderError(element, templateData); } else if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) { - this.renderLoadMore(element, templateData); + this.renderLoadMore(templateData); } } - private renderSession(session: ISession, data: ISessionTemplateData): void { + private renderSession(session: IDebugSession, data: ISessionTemplateData): void { data.session.title = nls.localize({ key: 'session', comment: ['Session is a noun'] }, "Session"); data.name.textContent = session.getName(this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE); const stoppedThread = session.getAllThreads().filter(t => t.stopped).pop(); @@ -506,7 +508,7 @@ class CallStackRenderer implements IRenderer { data.label.title = element; } - private renderLoadMore(element: any, data: ILoadMoreTemplateData): void { + private renderLoadMore(data: ILoadMoreTemplateData): void { data.label.textContent = nls.localize('loadMoreStackFrames', "Load More Stack Frames"); } @@ -515,7 +517,7 @@ class CallStackRenderer implements IRenderer { dom.toggleClass(data.stackFrame, 'label', stackFrame.presentationHint === 'label'); dom.toggleClass(data.stackFrame, 'subtle', stackFrame.presentationHint === 'subtle'); - data.file.title = stackFrame.source.raw.path || stackFrame.source.name; + data.file.title = stackFrame.source.inMemory ? stackFrame.source.name : this.labelService.getUriLabel(stackFrame.source.uri); if (stackFrame.source.raw.origin) { data.file.title += `\n${stackFrame.source.raw.origin}`; } diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index 4026503292e..c3c269519db 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -10,12 +10,13 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionRegistryExtensions } from 'vs/workbench/common/actions'; import { ToggleViewletAction, Extensions as ViewletExtensions, ViewletRegistry, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { TogglePanelAction, Extensions as PanelExtensions, PanelRegistry, PanelDescriptor } from 'vs/workbench/browser/panel'; -import { StatusbarItemDescriptor, StatusbarAlignment, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarItemDescriptor, IStatusbarRegistry, Extensions as StatusExtensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { VariablesView } from 'vs/workbench/parts/debug/electron-browser/variablesView'; import { BreakpointsView } from 'vs/workbench/parts/debug/browser/breakpointsView'; import { WatchExpressionsView } from 'vs/workbench/parts/debug/electron-browser/watchExpressionsView'; @@ -23,27 +24,27 @@ import { CallStackView } from 'vs/workbench/parts/debug/electron-browser/callSta import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, REPL_ID, CONTEXT_NOT_IN_DEBUG_MODE, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, VIEW_CONTAINER + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, VIEW_CONTAINER, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED } from 'vs/workbench/parts/debug/common/debug'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { DebugEditorModelManager } from 'vs/workbench/parts/debug/browser/debugEditorModelManager'; import { StepOverAction, ClearReplAction, FocusReplAction, StepIntoAction, StepOutAction, StartAction, RestartAction, ContinueAction, StopAction, DisconnectAction, PauseAction, AddFunctionBreakpointAction, - ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction, TerminateThreadAction + ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction, TerminateThreadAction, ToggleReplAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { DebugActionsWidget } from 'vs/workbench/parts/debug/browser/debugActionsWidget'; import * as service from 'vs/workbench/parts/debug/electron-browser/debugService'; import { DebugContentProvider } from 'vs/workbench/parts/debug/browser/debugContentProvider'; import 'vs/workbench/parts/debug/electron-browser/debugEditorContribution'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { registerCommands } from 'vs/workbench/parts/debug/browser/debugCommands'; +import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID } from 'vs/workbench/parts/debug/browser/debugCommands'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { StatusBarColorProvider } from 'vs/workbench/parts/debug/browser/statusbarColorProvider'; import { ViewsRegistry } from 'vs/workbench/common/views'; import { isMacintosh } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { DebugViewlet, FocusVariablesViewAction, FocusBreakpointsViewAction, FocusCallStackViewAction, FocusWatchViewAction } from 'vs/workbench/parts/debug/browser/debugViewlet'; import { Repl } from 'vs/workbench/parts/debug/electron-browser/repl'; import { DebugQuickOpenHandler } from 'vs/workbench/parts/debug/browser/debugQuickOpen'; @@ -51,6 +52,8 @@ import { DebugStatus } from 'vs/workbench/parts/debug/browser/debugStatus'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { LoadedScriptsView } from 'vs/workbench/parts/debug/browser/loadedScriptsView'; +import { TOGGLE_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID } from 'vs/workbench/parts/debug/browser/debugEditorActions'; class OpenDebugViewletAction extends ToggleViewletAction { public static readonly ID = VIEWLET_ID; @@ -111,6 +114,7 @@ Registry.as(PanelExtensions.Panels).setDefaultPanelId(REPL_ID); ViewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctor: VariablesView, order: 10, weight: 40, container: VIEW_CONTAINER, canToggleVisibility: true }]); ViewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctor: WatchExpressionsView, order: 20, weight: 10, container: VIEW_CONTAINER, canToggleVisibility: true }]); ViewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctor: CallStackView, order: 30, weight: 30, container: VIEW_CONTAINER, canToggleVisibility: true }]); +ViewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctor: LoadedScriptsView, order: 35, weight: 5, container: VIEW_CONTAINER, canToggleVisibility: true, collapsed: true, when: CONTEXT_LOADED_SCRIPTS_SUPPORTED }]); ViewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctor: BreakpointsView, order: 40, weight: 20, container: VIEW_CONTAINER, canToggleVisibility: true }]); // register action to open viewlet @@ -127,7 +131,7 @@ const debugCategory = nls.localize('debugCategory', "Debug"); registry.registerWorkbenchAction(new SyncActionDescriptor( StartAction, StartAction.ID, StartAction.LABEL, { primary: KeyCode.F5 }, CONTEXT_NOT_IN_DEBUG_MODE), 'Debug: Start Debugging', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(StepOverAction, StepOverAction.ID, StepOverAction.LABEL, { primary: KeyCode.F10 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Step Over', debugCategory); -registry.registerWorkbenchAction(new SyncActionDescriptor(StepIntoAction, StepIntoAction.ID, StepIntoAction.LABEL, { primary: KeyCode.F11 }, CONTEXT_IN_DEBUG_MODE, KeybindingsRegistry.WEIGHT.workbenchContrib(1)), 'Debug: Step Into', debugCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(StepIntoAction, StepIntoAction.ID, StepIntoAction.LABEL, { primary: KeyCode.F11 }, CONTEXT_IN_DEBUG_MODE, KeybindingWeight.WorkbenchContrib + 1), 'Debug: Step Into', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(StepOutAction, StepOutAction.ID, StepOutAction.LABEL, { primary: KeyMod.Shift | KeyCode.F11 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Step Out', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(RestartAction, RestartAction.ID, RestartAction.LABEL, { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Restart', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(StopAction, StopAction.ID, StopAction.LABEL, { primary: KeyMod.Shift | KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE), 'Debug: Stop', debugCategory); @@ -147,7 +151,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(FocusReplAction, Focus registry.registerWorkbenchAction(new SyncActionDescriptor(SelectAndStartAction, SelectAndStartAction.ID, SelectAndStartAction.LABEL), 'Debug: Select and Start Debugging', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusVariablesViewAction, FocusVariablesViewAction.ID, FocusVariablesViewAction.LABEL), 'Debug: Focus Variables', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusWatchViewAction, FocusWatchViewAction.ID, FocusWatchViewAction.LABEL), 'Debug: Focus Watch', debugCategory); -registry.registerWorkbenchAction(new SyncActionDescriptor(FocusCallStackViewAction, FocusCallStackViewAction.ID, FocusCallStackViewAction.LABEL), 'Debug: Focus CallStack', debugCategory); +registry.registerWorkbenchAction(new SyncActionDescriptor(FocusCallStackViewAction, FocusCallStackViewAction.ID, FocusCallStackViewAction.LABEL), 'Debug: Focus Call Stack', debugCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusBreakpointsViewAction, FocusBreakpointsViewAction.ID, FocusBreakpointsViewAction.LABEL), 'Debug: Focus Breakpoints', debugCategory); @@ -175,39 +179,39 @@ configurationRegistry.registerConfiguration({ properties: { 'debug.allowBreakpointsEverywhere': { type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allows setting breakpoint in any file"), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), default: false }, 'debug.openExplorerOnEnd': { type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'openExplorerOnEnd' }, "Automatically open explorer view on the end of a debug session"), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'openExplorerOnEnd' }, "Automatically open the explorer view at the end of a debug session"), default: false }, 'debug.inlineValues': { type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging"), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), default: false }, 'debug.toolBarLocation': { enum: ['floating', 'docked', 'hidden'], - description: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either \"floating\" in all views, \"docked\" in the debug view, or \"hidden\""), + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, or `hidden`"), default: 'floating' }, 'debug.showInStatusBar': { enum: ['never', 'always', 'onFirstSessionStart'], enumDescriptions: [nls.localize('never', "Never show debug in status bar"), nls.localize('always', "Always show debug in status bar"), nls.localize('onFirstSessionStart', "Show debug in status bar only after debug was started for the first time")], - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug status bar should be visible"), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug status bar should be visible."), default: 'onFirstSessionStart' }, 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, 'debug.openDebug': { enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], - default: 'openOnFirstSessionStart', - description: nls.localize('openDebug', "Controls whether debug view should be open on debugging session start.") + default: 'openOnSessionStart', + description: nls.localize('openDebug', "Controls when the debug view should open.") }, 'debug.enableAllHovers': { type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'enableAllHovers' }, "Controls if the non debug hovers should be enabled while debugging. If true the hover providers will be called to provide a hover. Regular hovers will not be shown even if this setting is true."), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'enableAllHovers' }, "Controls whether the non-debug hovers should be enabled while debugging. When enabled the hover providers will be called to provide a hover. Regular hovers will not be shown even if this setting is enabled."), default: false }, 'launch': { @@ -225,13 +229,224 @@ registerCommands(); const statusBar = Registry.as(StatusExtensions.Statusbar); statusBar.registerStatusbarItem(new StatusbarItemDescriptor(DebugStatus, StatusbarAlignment.LEFT, 30 /* Low Priority */)); +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: ToggleReplAction.ID, + title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + order: 2 +}); + +// Debug menu + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: StartAction.ID, + title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RunAction.ID, + title: nls.localize({ key: 'miStartWithoutDebugging', comment: ['&& denotes a mnemonic'] }, "Start &&Without Debugging") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: StopAction.ID, + title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: RestartAction.ID, + title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"), + precondition: CONTEXT_IN_DEBUG_MODE + }, + order: 4 +}); + +// Configuration +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ConfigureAction.ID, + title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: ADD_CONFIGURATION_ID, + title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "Add Configuration...") + }, + order: 2 +}); + +// Step Commands +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepOverAction.ID, + title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepIntoAction.ID, + title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: StepOutAction.ID, + title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: ContinueAction.ID, + title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"), + precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') + }, + order: 4 +}); + +// New Breakpoints +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: TOGGLE_BREAKPOINT_ID, + title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, + title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: TOGGLE_INLINE_BREAKPOINT_ID, + title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Inline Breakp&&oint") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: AddFunctionBreakpointAction.ID, + title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { + group: '1_breakpoints', + command: { + id: TOGGLE_LOG_POINT_ID, + title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") + }, + order: 4 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), + submenu: MenuId.MenubarNewBreakpointMenu, + order: 2 +}); + +// Modify Breakpoints +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: EnableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Enable All Breakpoints") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: DisableAllBreakpointsAction.ID, + title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: RemoveAllBreakpointsAction.ID, + title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + order: 3 +}); + +// Install Debuggers +MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: 'z_install', + command: { + id: 'debug.installAdditionalDebuggers', + title: nls.localize({ key: 'miInstallAdditionalDebuggers', comment: ['&& denotes a mnemonic'] }, "&&Install Additional Debuggers...") + }, + order: 1 +}); + // Touch Bar if (isMacintosh) { const registerTouchBarEntry = (id: string, title: string, order, when: ContextKeyExpr, icon: string) => { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { - id, title, iconPath: { dark: URI.parse(require.toUrl(`vs/workbench/parts/debug/electron-browser/media/${icon}`)).fsPath } + id, title, iconLocation: { dark: URI.parse(require.toUrl(`vs/workbench/parts/debug/electron-browser/media/${icon}`)) } }, when, group: '9_debug', diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index da5740eba43..0062a0fdc73 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -9,8 +9,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; -import uri from 'vs/base/common/uri'; -import * as paths from 'vs/base/common/paths'; +import { URI as uri } from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ITextModel } from 'vs/editor/common/model'; import { IEditor } from 'vs/workbench/common/editor'; @@ -25,7 +25,6 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter, ITerminalSettings, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -33,6 +32,7 @@ import { TerminalLauncher } from 'vs/workbench/parts/debug/electron-browser/term import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { launchSchema, debuggersExtPoint, breakpointsExtPoint } from 'vs/workbench/parts/debug/common/debugSchemas'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -57,7 +57,7 @@ export class ConfigurationManager implements IConfigurationManager { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IEditorService private editorService: IEditorService, @IConfigurationService private configurationService: IConfigurationService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IInstantiationService private instantiationService: IInstantiationService, @ICommandService private commandService: ICommandService, @IStorageService private storageService: IStorageService, @@ -347,10 +347,11 @@ export class ConfigurationManager implements IConfigurationManager { } candidates = candidates.sort((first, second) => first.label.localeCompare(second.label)); - return this.quickOpenService.pick([...candidates, { label: 'More...', separator: { border: true } }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) + const picks = candidates.map(c => ({ label: c.label, debugger: c })); + return this.quickInputService.pick<(typeof picks)[0]>([...picks, { type: 'separator' }, { label: 'More...', debugger: undefined }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) .then(picked => { - if (picked instanceof Debugger) { - return picked; + if (picked && picked.debugger) { + return picked.debugger; } if (picked) { this.commandService.executeCommand('debug.installAdditionalDebuggers'); @@ -390,7 +391,7 @@ class Launch implements ILaunch { } public get uri(): uri { - return this.workspace.uri.with({ path: paths.join(this.workspace.uri.path, '/.vscode/launch.json') }); + return resources.joinPath(this.workspace.uri, '/.vscode/launch.json'); } public get name(): string { @@ -441,15 +442,13 @@ class Launch implements ILaunch { return config.configurations.filter(config => config && config.name === name).shift(); } - public openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { - return this.configurationManager.activateDebuggers().then(() => { - const resource = this.uri; - let created = false; - - return this.fileService.resolveContent(resource).then(content => content.value, err => { - - // launch.json not found: create one by collecting launch configs from debugConfigProviders + public openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { + const resource = this.uri; + let created = false; + return this.fileService.resolveContent(resource).then(content => content.value, err => { + // launch.json not found: create one by collecting launch configs from debugConfigProviders + return this.configurationManager.activateDebuggers().then(() => { return this.configurationManager.guessDebugger(type).then(adapter => { if (adapter) { return this.configurationManager.provideDebugConfigurations(this.workspace.uri, adapter.type).then(initialConfigs => { @@ -470,31 +469,31 @@ class Launch implements ILaunch { return content; }); }); - }).then(content => { - if (!content) { - return undefined; - } - const index = content.indexOf(`"${this.configurationManager.selectedConfiguration.name}"`); - let startLineNumber = 1; - for (let i = 0; i < index; i++) { - if (content.charAt(i) === '\n') { - startLineNumber++; - } - } - const selection = startLineNumber > 1 ? { startLineNumber, startColumn: 4 } : undefined; - - return this.editorService.openEditor({ - resource: resource, - options: { - forceOpen: true, - selection, - pinned: created, - revealIfVisible: true - }, - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created })); - }, (error) => { - throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); }); + }).then(content => { + if (!content) { + return { editor: undefined, created: false }; + } + const index = content.indexOf(`"${this.configurationManager.selectedConfiguration.name}"`); + let startLineNumber = 1; + for (let i = 0; i < index; i++) { + if (content.charAt(i) === '\n') { + startLineNumber++; + } + } + const selection = startLineNumber > 1 ? { startLineNumber, startColumn: 4 } : undefined; + + return this.editorService.openEditor({ + resource, + options: { + selection, + preserveFocus, + pinned: created, + revealIfVisible: true + }, + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created })); + }, (error) => { + throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); }); } } @@ -523,8 +522,11 @@ class WorkspaceLaunch extends Launch implements ILaunch { return this.configurationService.inspect('launch').workspace; } - openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { - return this.editorService.openEditor({ resource: this.contextService.getWorkspace().configuration }).then(editor => ({ editor, created: false })); + openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { + return this.editorService.openEditor({ + resource: this.contextService.getWorkspace().configuration, + options: { preserveFocus } + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created: false })); } } @@ -557,7 +559,7 @@ class UserLaunch extends Launch implements ILaunch { return this.configurationService.inspect('launch').user; } - openConfigFile(sideBySide: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { - return this.preferencesService.openGlobalSettings().then(editor => ({ editor, created: false })); + openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string): TPromise<{ editor: IEditor, created: boolean }> { + return this.preferencesService.openGlobalSettings(false, { preserveFocus }).then(editor => ({ editor, created: false })); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 06da05910b3..c884878dd4e 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as lifecycle from 'vs/base/common/lifecycle'; import * as env from 'vs/base/common/platform'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { visit } from 'vs/base/common/json'; import severity from 'vs/base/common/severity'; import { Constants } from 'vs/editor/common/core/uint'; @@ -21,12 +20,12 @@ import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -46,6 +45,8 @@ import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { memoize } from 'vs/base/common/decorators'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; +import { CancellationToken } from 'vs/base/common/cancellation'; const HOVER_DELAY = 300; const LAUNCH_JSON_REGEX = /launch\.json$/; @@ -262,7 +263,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; const model = this.editor.getModel(); - this.editor.updateOptions({ hover: !stackFrame || !model || model.uri.toString() !== stackFrame.source.uri.toString() }); + this._applyHoverConfiguration(model, stackFrame); this.closeBreakpointWidget(); this.toggleExceptionWidget(); this.hideHoverWidget(); @@ -278,6 +279,32 @@ export class DebugEditorContribution implements IDebugEditorContribution { })); } + private _applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame): void { + if (stackFrame && model && model.uri.toString() === stackFrame.source.uri.toString()) { + this.editor.updateOptions({ + hover: { + enabled: !stackFrame || !model || model.uri.toString() !== stackFrame.source.uri.toString() + } + }); + } else { + let overrides: IConfigurationOverrides; + if (model) { + overrides = { + resource: model.uri, + overrideIdentifier: model.getLanguageIdentifier().language + }; + } + const defaultConfiguration = this.configurationService.getValue('editor.hover', overrides); + this.editor.updateOptions({ + hover: { + enabled: defaultConfiguration.enabled, + delay: defaultConfiguration.delay, + sticky: defaultConfiguration.sticky + } + }); + } + } + public getId(): string { return EDITOR_CONTRIBUTION_ID; } @@ -323,11 +350,10 @@ export class DebugEditorContribution implements IDebugEditorContribution { private onFocusStackFrame(sf: IStackFrame): void { const model = this.editor.getModel(); + this._applyHoverConfiguration(model, sf); if (model && sf && sf.source.uri.toString() === model.uri.toString()) { - this.editor.updateOptions({ hover: false }); this.toggleExceptionWidget(); } else { - this.editor.updateOptions({ hover: true }); this.hideHoverWidget(); } @@ -353,7 +379,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { @memoize private get provideNonDebugHoverScheduler(): RunOnceScheduler { const scheduler = new RunOnceScheduler(() => { - getHover(this.editor.getModel(), this.nonDebugHoverPosition); + getHover(this.editor.getModel(), this.nonDebugHoverPosition, CancellationToken.None); }, HOVER_DELAY); this.toDispose.push(scheduler); @@ -489,7 +515,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (model && LAUNCH_JSON_REGEX.test(model.uri.toString())) { this.configurationWidget = this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('addConfiguration', "Add Configuration..."), null); this.configurationWidget.render(); - this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration().done(undefined, errors.onUnexpectedError))); + this.toDispose.push(this.configurationWidget.onClick(() => this.addLaunchConfiguration())); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index 6e43c34d40b..0777fb627fa 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -152,7 +152,7 @@ export class DebugHoverWidget implements IContentWidget { } let promise: TPromise; - if (session.raw.capabilities.supportsEvaluateForHovers) { + if (session.capabilities.supportsEvaluateForHovers) { const result = new Expression(matchingExpression); promise = result.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover').then(() => result); } else { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 135d6e164a9..c9aa576f4b9 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -4,13 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as lifecycle from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as resources from 'vs/base/common/resources'; -import * as strings from 'vs/base/common/strings'; -import { generateUuid } from 'vs/base/common/uuid'; -import uri from 'vs/base/common/uri'; -import * as platform from 'vs/base/common/platform'; +import { URI as uri } from 'vs/base/common/uri'; import { first, distinct } from 'vs/base/common/arrays'; import { isObject, isUndefinedOrNull } from 'vs/base/common/types'; import * as errors from 'vs/base/common/errors'; @@ -20,15 +16,13 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import * as debug from 'vs/workbench/parts/debug/common/debug'; -import { RawDebugSession } from 'vs/workbench/parts/debug/electron-browser/rawDebugSession'; -import { Model, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression, RawObjectReplElement, ExpressionContainer, Session, Thread } from 'vs/workbench/parts/debug/common/debugModel'; +import { Model, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression, RawObjectReplElement } from 'vs/workbench/parts/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel'; import * as debugactions from 'vs/workbench/parts/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/parts/debug/electron-browser/debugConfigurationManager'; @@ -43,18 +37,18 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; -import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService'; +import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; +import { IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService'; import { IRemoteConsoleLog, parse, getFirstFrame } from 'vs/base/node/console'; -import { Source } from 'vs/workbench/parts/debug/common/debugSource'; -import { TaskEvent, TaskEventKind } from 'vs/workbench/parts/tasks/common/tasks'; +import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import product from 'vs/platform/node/product'; import { deepClone, equals } from 'vs/base/common/objects'; +import { DebugSession } from 'vs/workbench/parts/debug/electron-browser/debugSession'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IModel, IReplElementSource, IEnablement, IBreakpoint, IBreakpointData, IExpression, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent } from 'vs/workbench/parts/debug/common/debug'; +import { isExtensionHostDebugging } from 'vs/workbench/parts/debug/common/debugUtils'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -62,28 +56,25 @@ const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; const DEBUG_EXCEPTION_BREAKPOINTS_KEY = 'debug.exceptionbreakpoint'; const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; -export class DebugService implements debug.IDebugService { - public _serviceBrand: any; +export class DebugService implements IDebugService { + _serviceBrand: any; - private sessionStates: Map; - private readonly _onDidChangeState: Emitter; - private readonly _onDidNewSession: Emitter; - private readonly _onDidEndSession: Emitter; - private readonly _onDidCustomEvent: Emitter; + private readonly _onDidChangeState: Emitter; + private readonly _onDidNewSession: Emitter; + private readonly _onWillNewSession: Emitter; + private readonly _onDidEndSession: Emitter; private model: Model; private viewModel: ViewModel; - private allSessions: Map; private configurationManager: ConfigurationManager; - private toDispose: lifecycle.IDisposable[]; - private toDisposeOnSessionEnd: Map; - private inDebugMode: IContextKey; + private allSessions = new Map(); + private toDispose: IDisposable[]; private debugType: IContextKey; private debugState: IContextKey; + private inDebugMode: IContextKey; private breakpointsToSendOnResourceSaved: Set; - private firstSessionStart: boolean; private skipRunningTask: boolean; - private previousState: debug.State; - private fetchThreadsSchedulers: Map; + private initializing = false; + private previousState: State; constructor( @IStorageService private storageService: IStorageService, @@ -108,159 +99,656 @@ export class DebugService implements debug.IDebugService { @IConfigurationService private configurationService: IConfigurationService, ) { this.toDispose = []; - this.toDisposeOnSessionEnd = new Map(); + this.breakpointsToSendOnResourceSaved = new Set(); - this._onDidChangeState = new Emitter(); - this._onDidNewSession = new Emitter(); - this._onDidEndSession = new Emitter(); - this._onDidCustomEvent = new Emitter(); - this.sessionStates = new Map(); - this.allSessions = new Map(); - this.fetchThreadsSchedulers = new Map(); + + this._onDidChangeState = new Emitter(); + this._onDidNewSession = new Emitter(); + this._onWillNewSession = new Emitter(); + this._onDidEndSession = new Emitter(); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager); this.toDispose.push(this.configurationManager); - this.inDebugMode = debug.CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); - this.debugType = debug.CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); - this.debugState = debug.CONTEXT_DEBUG_STATE.bindTo(contextKeyService); + + this.debugType = CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); + this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService); + this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.model = new Model(this.loadBreakpoints(), this.storageService.getBoolean(DEBUG_BREAKPOINTS_ACTIVATED_KEY, StorageScope.WORKSPACE, true), this.loadFunctionBreakpoints(), - this.loadExceptionBreakpoints(), this.loadWatchExpressions()); + this.loadExceptionBreakpoints(), this.loadWatchExpressions(), this.textFileService); this.toDispose.push(this.model); + this.viewModel = new ViewModel(contextKeyService); - this.firstSessionStart = true; - this.registerListeners(); - } - - private registerListeners(): void { this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); this.lifecycleService.onShutdown(this.store, this); this.lifecycleService.onShutdown(this.dispose, this); - this.toDispose.push(this.broadcastService.onBroadcast(this.onBroadcast, this)); + + this.toDispose.push(this.broadcastService.onBroadcast(broadcast => { + const session = this.getSession(broadcast.payload.debugId); + if (session) { + switch (broadcast.channel) { + + case EXTENSION_ATTACH_BROADCAST_CHANNEL: + // EH was started in debug mode -> attach to it + this.attachExtensionHost(session, broadcast.payload.port); + break; + + case EXTENSION_TERMINATE_BROADCAST_CHANNEL: + // EH was terminated + session.disconnect(); + break; + + case EXTENSION_LOG_BROADCAST_CHANNEL: + // extension logged output -> show it in REPL + this.addToRepl(session, broadcast.payload.logEntry); + break; + } + } + }, this)); + this.toDispose.push(this.viewModel.onDidFocusSession(s => { const id = s ? s.getId() : undefined; this.model.setBreakpointsSessionId(id); + this.onStateChange(); })); } - private onBroadcast(broadcast: IBroadcast): void { + getSession(sessionId: string): IDebugSession { + return this.allSessions.get(sessionId); + } - // attach: PH is ready to be attached to - const session = this.allSessions.get(broadcast.payload.debugId); - if (!session) { - // Ignore attach events for sessions that never existed (wrong vscode windows) - return; - } - const raw = session.raw; + getModel(): IModel { + return this.model; + } - if (broadcast.channel === EXTENSION_ATTACH_BROADCAST_CHANNEL) { - const initialAttach = session.configuration.request === 'launch'; + getViewModel(): IViewModel { + return this.viewModel; + } - session.configuration.request = 'attach'; - session.configuration.port = broadcast.payload.port; - // Do not end process on initial attach (since the request is still 'launch') - if (initialAttach) { - const root = raw.root; - lifecycle.dispose(this.toDisposeOnSessionEnd[raw.getId()]); - this.initializeRawSession(root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId(), session).then(session => { - (session.raw).attach(session.configuration); - }); - } else { - this.onRawSessionEnd(raw); - this.doCreateSession(session.raw.root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId()); - } + getConfigurationManager(): IConfigurationManager { + return this.configurationManager; + } - return; + sourceIsNotAvailable(uri: uri): void { + this.model.sourceIsNotAvailable(uri); + } + + dispose(): void { + this.toDispose = dispose(this.toDispose); + } + + //---- state management + + get state(): State { + const focusedSession = this.viewModel.focusedSession; + if (focusedSession) { + return focusedSession.state; } - if (broadcast.channel === EXTENSION_TERMINATE_BROADCAST_CHANNEL) { - this.onRawSessionEnd(raw); - return; - } + return this.initializing ? State.Initializing : State.Inactive; + } - // an extension logged output, show it inside the REPL - if (broadcast.channel === EXTENSION_LOG_BROADCAST_CHANNEL) { - let extensionOutput: IRemoteConsoleLog = broadcast.payload.logEntry; - let sev = extensionOutput.severity === 'warn' ? severity.Warning : extensionOutput.severity === 'error' ? severity.Error : severity.Info; - - const { args, stack } = parse(extensionOutput); - let source: debug.IReplElementSource; - if (stack) { - const frame = getFirstFrame(stack); - if (frame) { - source = { - column: frame.column, - lineNumber: frame.line, - source: session.getSource({ - name: resources.basenameOrAuthority(frame.uri), - path: frame.uri.fsPath - }) - }; - } - } - - // add output for each argument logged - let simpleVals: any[] = []; - for (let i = 0; i < args.length; i++) { - let a = args[i]; - - // undefined gets printed as 'undefined' - if (typeof a === 'undefined') { - simpleVals.push('undefined'); - } - - // null gets printed as 'null' - else if (a === null) { - simpleVals.push('null'); - } - - // objects & arrays are special because we want to inspect them in the REPL - else if (isObject(a) || Array.isArray(a)) { - - // flush any existing simple values logged - if (simpleVals.length) { - this.logToRepl(simpleVals.join(' '), sev, source); - simpleVals = []; - } - - // show object - this.logToRepl(new RawObjectReplElement((a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); - } - - // string: watch out for % replacement directive - // string substitution and formatting @ https://developer.chrome.com/devtools/docs/console - else if (typeof a === 'string') { - let buf = ''; - - for (let j = 0, len = a.length; j < len; j++) { - if (a[j] === '%' && (a[j + 1] === 's' || a[j + 1] === 'i' || a[j + 1] === 'd')) { - i++; // read over substitution - buf += !isUndefinedOrNull(args[i]) ? args[i] : ''; // replace - j++; // read over directive - } else { - buf += a[j]; - } - } - - simpleVals.push(buf); - } - - // number or boolean is joined together - else { - simpleVals.push(a); - } - } - - // flush simple values - // always append a new line for output coming from an extension such that separate logs go to separate lines #23695 - if (simpleVals.length) { - this.logToRepl(simpleVals.join(' ') + '\n', sev, source); - } + private startInitializingState() { + if (!this.initializing) { + this.initializing = true; + this.onStateChange(); } } - private tryToAutoFocusStackFrame(thread: debug.IThread): TPromise { + private endInitializingState() { + if (this.initializing) { + this.initializing = false; + this.onStateChange(); + } + } + + private onStateChange(): void { + const state = this.state; + if (this.previousState !== state) { + const stateLabel = State[state]; + if (stateLabel) { + this.debugState.set(stateLabel.toLowerCase()); + this.inDebugMode.set(state !== State.Inactive); + } + this.previousState = state; + this._onDidChangeState.fire(state); + } + } + + get onDidChangeState(): Event { + return this._onDidChangeState.event; + } + + get onDidNewSession(): Event { + return this._onDidNewSession.event; + } + + get onWillNewSession(): Event { + return this._onWillNewSession.event; + } + + get onDidEndSession(): Event { + return this._onDidEndSession.event; + } + + //---- life cycle management + + /** + * main entry point + */ + startDebugging(launch: ILaunch, configOrName?: IConfig | string, noDebug = false, unresolvedConfiguration?: IConfig, ): TPromise { + + // make sure to save all files and that the configuration is up to date + return this.extensionService.activateByEvent('onDebug').then(() => + this.textFileService.saveAll().then(() => + this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined).then(() => + this.extensionService.whenInstalledExtensionsRegistered().then(() => { + + if (this.model.getSessions().length === 0) { + this.removeReplExpressions(); + this.allSessions.clear(); + } + + let config: IConfig, compound: ICompound; + if (!configOrName) { + configOrName = this.configurationManager.selectedConfiguration.name; + } + if (typeof configOrName === 'string' && launch) { + config = launch.getConfiguration(configOrName); + compound = launch.getCompound(configOrName); + + const sessions = this.model.getSessions(); + const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); + if (sessions.some(s => s.getName(false) === configOrName && (!launch || !launch.workspace || !s.root || s.root.uri.toString() === launch.workspace.uri.toString()))) { + return TPromise.wrapError(new Error(alreadyRunningMessage)); + } + if (compound && compound.configurations && sessions.some(p => compound.configurations.indexOf(p.getName(false)) !== -1)) { + return TPromise.wrapError(new Error(alreadyRunningMessage)); + } + } else if (typeof configOrName !== 'string') { + config = configOrName; + } + + if (compound) { + if (!compound.configurations) { + return TPromise.wrapError(new Error(nls.localize({ key: 'compoundMustHaveConfigurations', comment: ['compound indicates a "compounds" configuration item', '"configurations" is an attribute and should not be localized'] }, + "Compound must have \"configurations\" attribute set in order to start multiple configurations."))); + } + + return TPromise.join(compound.configurations.map(configData => { + const name = typeof configData === 'string' ? configData : configData.name; + if (name === compound.name) { + return TPromise.as(null); + } + + let launchForName: ILaunch; + if (typeof configData === 'string') { + const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)); + if (launchesContainingName.length === 1) { + launchForName = launchesContainingName[0]; + } else if (launchesContainingName.length > 1 && launchesContainingName.indexOf(launch) >= 0) { + // If there are multiple launches containing the configuration give priority to the configuration in the current launch + launchForName = launch; + } else { + return TPromise.wrapError(new Error(launchesContainingName.length === 0 ? nls.localize('noConfigurationNameInWorkspace', "Could not find launch configuration '{0}' in the workspace.", name) + : nls.localize('multipleConfigurationNamesInWorkspace', "There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name))); + } + } else if (configData.folder) { + const launchesMatchingConfigData = this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.name === configData.folder && !!l.getConfiguration(configData.name)); + if (launchesMatchingConfigData.length === 1) { + launchForName = launchesMatchingConfigData[0]; + } else { + return TPromise.wrapError(new Error(nls.localize('noFolderWithName', "Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", configData.folder, configData.name, compound.name))); + } + } + + return this.startDebugging(launchForName, name, noDebug, unresolvedConfiguration); + })); + } + if (configOrName && !config) { + const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", typeof configOrName === 'string' ? configOrName : JSON.stringify(configOrName)) : + nls.localize('launchJsonDoesNotExist', "'launch.json' does not exist."); + return TPromise.wrapError(new Error(message)); + } + + // We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes. + // Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config. + let type: string; + if (config) { + type = config.type; + } else { + // a no-folder workspace has no launch.config + config = {}; + } + unresolvedConfiguration = unresolvedConfiguration || deepClone(config); + + if (noDebug) { + config.noDebug = true; + } + + return (type ? TPromise.as(null) : this.configurationManager.guessDebugger().then(a => type = a && a.type)).then(() => + this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { + // a falsy config indicates an aborted launch + if (config && config.type) { + return this.createSession(launch, config, unresolvedConfiguration); + } + + if (launch && type) { + return launch.openConfigFile(false, true, type).then(() => undefined); + } + + return undefined; + }) + ).then(() => undefined); + }) + ))); + } + + private createSession(launch: ILaunch, config: IConfig, unresolvedConfig: IConfig): TPromise { + + this.startInitializingState(); + + return this.textFileService.saveAll().then(() => + this.substituteVariables(launch, config).then(resolvedConfig => { + + if (!resolvedConfig) { + // User canceled resolving of interactive variables, silently return + return undefined; + } + + if (!this.configurationManager.getDebugger(resolvedConfig.type) || (config.request !== 'attach' && config.request !== 'launch')) { + let message: string; + if (config.request !== 'attach' && config.request !== 'launch') { + message = config.request ? nls.localize('debugRequestNotSupported', "Attribute '{0}' has an unsupported value '{1}' in the chosen debug configuration.", 'request', config.request) + : nls.localize('debugRequesMissing', "Attribute '{0}' is missing from the chosen debug configuration.", 'request'); + + } else { + message = resolvedConfig.type ? nls.localize('debugTypeNotSupported', "Configured debug type '{0}' is not supported.", resolvedConfig.type) : + nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration."); + } + + return this.showError(message); + } + + const workspace = launch ? launch.workspace : undefined; + return this.runTask(workspace, resolvedConfig.preLaunchTask, resolvedConfig, unresolvedConfig).then(success => { + if (success) { + return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }); + } + return undefined; + }); + }, err => { + if (err && err.message) { + return this.showError(err.message); + } + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved on disk and that you have a debug extension installed for that file type.")); + } + + return launch && launch.openConfigFile(false, true).then(editor => void 0); + }) + ).then(() => { + this.endInitializingState(); + }, err => { + this.endInitializingState(); + return TPromise.wrapError(err); + }); + } + + private attachExtensionHost(session: IDebugSession, port: number): TPromise { + + session.configuration.request = 'attach'; + session.configuration.port = port; + const dbgr = this.configurationManager.getDebugger(session.configuration.type); + + return session.initialize(dbgr).then(() => { + session.launchOrAttach(session.configuration).then(() => { + this.focusStackFrame(undefined, undefined, session); + }); + }); + } + + private doCreateSession(root: IWorkspaceFolder, configuration: { resolved: IConfig, unresolved: IConfig }): TPromise { + + const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model); + this.allSessions.set(session.getId(), session); + + // register listeners as the very first thing! + this.registerSessionListeners(session); + + // since the Session is now properly registered under its ID and hooked, we can announce it + // this event doesn't go to extensions + this._onWillNewSession.fire(session); + + const resolved = configuration.resolved; + const dbgr = this.configurationManager.getDebugger(resolved.type); + + return session.initialize(dbgr).then(() => { + + return session.launchOrAttach(resolved).then(() => { + + this.focusStackFrame(undefined, undefined, session); + + // since the initialized response has arrived announce the new Session (including extensions) + this._onDidNewSession.fire(session); + + const internalConsoleOptions = resolved.internalConsoleOptions || this.configurationService.getValue('debug').internalConsoleOptions; + if (internalConsoleOptions === 'openOnSessionStart' || (this.viewModel.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) { + this.panelService.openPanel(REPL_ID, false); + } + + const openDebug = this.configurationService.getValue('debug').openDebug; + // Open debug viewlet based on the visibility of the side bar and openDebug setting + if (openDebug === 'openOnSessionStart' || (openDebug === 'openOnFirstSessionStart' && this.viewModel.firstSessionStart)) { + this.viewletService.openViewlet(VIEWLET_ID); + } + this.viewModel.firstSessionStart = false; + + this.debugType.set(resolved.type); + if (this.model.getSessions().length > 1) { + this.viewModel.setMultiSessionView(true); + } + + return this.telemetryDebugSessionStart(root, resolved.type, dbgr.extensionDescription); + + }).then(() => session, (error: Error | string) => { + + if (session) { + session.dispose(); + } + + if (errors.isPromiseCanceledError(error)) { + // don't show 'canceled' error messages to the user #7906 + return TPromise.as(undefined); + } + + // Show the repl if some error got logged there #5870 + if (this.model.getReplElements().length > 0) { + this.panelService.openPanel(REPL_ID, false); + } + + if (resolved && resolved.request === 'attach' && resolved.__autoAttach) { + // ignore attach timeouts in auto attach mode + } else { + const errorMessage = error instanceof Error ? error.message : error; + this.telemetryDebugMisconfiguration(resolved ? resolved.type : undefined, errorMessage); + this.showError(errorMessage, errors.isErrorWithActions(error) ? error.actions : []); + } + return TPromise.as(undefined); + }); + + }).then(undefined, error => { + + if (session) { + session.dispose(); + } + + if (errors.isPromiseCanceledError(error)) { + // don't show 'canceled' error messages to the user #7906 + return TPromise.as(null); + } + return TPromise.wrapError(error); + }); + } + + private registerSessionListeners(session: IDebugSession): void { + + this.toDispose.push(session.onDidChangeState((state) => { + if (state === State.Running && this.viewModel.focusedSession && this.viewModel.focusedSession.getId() === session.getId()) { + this.focusStackFrame(undefined); + } + this.onStateChange(); + })); + + this.toDispose.push(session.onDidEndAdapter(adapterExitEvent => { + + if (adapterExitEvent.error) { + this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString())); + } + + // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 + if (isExtensionHostDebugging(session.configuration) && session.state === State.Running && session.configuration.noDebug) { + this.broadcastService.broadcast({ + channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, + payload: [session.root.uri.toString()] + }); + } + + this.telemetryDebugSessionStop(session, adapterExitEvent); + + if (session.configuration.postDebugTask) { + this.doRunTask(session.root, session.configuration.postDebugTask).then(undefined, err => + this.notificationService.error(err) + ); + } + session.dispose(); + this._onDidEndSession.fire(session); + + const focusedSession = this.viewModel.focusedSession; + if (focusedSession && focusedSession.getId() === session.getId()) { + this.focusStackFrame(undefined); + } + + if (this.model.getSessions().length === 0) { + this.debugType.reset(); + this.viewModel.setMultiSessionView(false); + + if (this.partService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue('debug').openExplorerOnEnd) { + this.viewletService.openViewlet(EXPLORER_VIEWLET_ID); + } + } + + })); + } + + restartSession(session: IDebugSession, restartData?: any): TPromise { + + + return this.textFileService.saveAll().then(() => { + + const unresolvedConfiguration = session.unresolvedConfiguration; + if (session.capabilities.supportsRestartRequest) { + return this.runTask(session.root, session.configuration.postDebugTask, session.configuration, unresolvedConfiguration) + .then(success => success ? this.runTask(session.root, session.configuration.preLaunchTask, session.configuration, unresolvedConfiguration) + .then(success => success ? session.restart() : undefined) : TPromise.as(undefined)); + } + + const focusedSession = this.viewModel.focusedSession; + const preserveFocus = focusedSession && session.getId() === focusedSession.getId(); + + // Do not run preLaunch and postDebug tasks for automatic restarts + const isAutoRestart = !!restartData; + this.skipRunningTask = isAutoRestart; + + if (isExtensionHostDebugging(session.configuration) && session.root) { + return this.broadcastService.broadcast({ + channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, + payload: [session.root.uri.toString()] + }); + } + + // If the restart is automatic -> disconnect, otherwise -> terminate #55064 + return (isAutoRestart ? session.disconnect(true) : session.terminate(true)).then(() => { + + return new TPromise((c, e) => { + setTimeout(() => { + // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration + let configToUse = session.configuration; + + const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined; + if (launch) { + const config = launch.getConfiguration(session.configuration.name); + if (config && !equals(config, unresolvedConfiguration)) { + // Take the type from the session since the debug extension might overwrite it #21316 + configToUse = config; + configToUse.type = session.configuration.type; + configToUse.noDebug = session.configuration.noDebug; + } + } + configToUse.__restart = restartData; + this.skipRunningTask = isAutoRestart; + this.startDebugging(launch, configToUse, configToUse.noDebug, unresolvedConfiguration).then(() => c(null), err => e(err)); + }, 300); + }); + }).then(() => { + if (preserveFocus) { + // Restart should preserve the focused session + const restartedSession = this.model.getSessions().filter(p => p.configuration.name === session.configuration.name).pop(); + if (restartedSession && restartedSession !== this.viewModel.focusedSession) { + this.focusStackFrame(undefined, undefined, restartedSession); + } + } + }); + }); + } + + stopSession(session: IDebugSession): TPromise { + + if (session) { + return session.terminate(); + } + + const sessions = this.model.getSessions(); + if (sessions.length) { + return TPromise.join(sessions.map(s => s.terminate())); + } + + this._onDidChangeState.fire(); // TODO@AW why state change? + return undefined; + } + + private substituteVariables(launch: ILaunch | undefined, config: IConfig): TPromise { + const dbg = this.configurationManager.getDebugger(config.type); + if (dbg) { + let folder: IWorkspaceFolder = undefined; + if (launch && launch.workspace) { + folder = launch.workspace; + } else { + const folders = this.contextService.getWorkspace().folders; + if (folders.length === 1) { + folder = folders[0]; + } + } + return dbg.substituteVariables(folder, config).then(config => { + return config; + }, (err: Error) => { + this.showError(err.message); + return undefined; // bail out + }); + } + return TPromise.as(config); + } + + private showError(message: string, actions: IAction[] = []): TPromise { + const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); + actions.push(configureAction); + return this.dialogService.show(severity.Error, message, actions.map(a => a.label).concat(nls.localize('cancel', "Cancel")), { cancelId: actions.length }).then(choice => { + if (choice < actions.length) { + return actions[choice].run(); + } + + return TPromise.as(null); + }); + } + + //---- task management + + private runTask(root: IWorkspaceFolder, taskId: string | TaskIdentifier, config: IConfig, unresolvedConfig: IConfig): TPromise { + + const debugAnywayAction = new Action('debug.debugAnyway', nls.localize('debugAnyway', "Debug Anyway"), undefined, true, () => { + return this.doCreateSession(root, { resolved: config, unresolved: unresolvedConfig }); + }); + + return this.doRunTask(root, taskId).then((taskSummary: ITaskSummary) => { + const errorCount = config.preLaunchTask ? this.markerService.getStatistics().errors : 0; + const successExitCode = taskSummary && taskSummary.exitCode === 0; + const failureExitCode = taskSummary && taskSummary.exitCode !== undefined && taskSummary.exitCode !== 0; + if (successExitCode || (errorCount === 0 && !failureExitCode)) { + return true; + } + + const taskId = typeof config.preLaunchTask === 'string' ? config.preLaunchTask : JSON.stringify(config.preLaunchTask); + const message = errorCount > 1 + ? nls.localize('preLaunchTaskErrors', "Build errors have been detected during preLaunchTask '{0}'.", taskId) + : errorCount === 1 + ? nls.localize('preLaunchTaskError', "Build error has been detected during preLaunchTask '{0}'.", taskId) + : nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", taskId, taskSummary.exitCode); + + const showErrorsAction = new Action('debug.showErrors', nls.localize('showErrors', "Show Errors"), undefined, true, () => { + return this.panelService.openPanel(Constants.MARKERS_PANEL_ID).then(() => undefined); + }); + + return this.showError(message, [debugAnywayAction, showErrorsAction]).then(() => false); + }, (err: TaskError) => { + return this.showError(err.message, [debugAnywayAction, this.taskService.configureAction()]).then(() => false); + }); + } + + private doRunTask(root: IWorkspaceFolder, taskId: string | TaskIdentifier): TPromise { + if (!taskId || this.skipRunningTask) { + this.skipRunningTask = false; + return TPromise.as(null); + } + // run a task before starting a debug session + return this.taskService.getTask(root, taskId).then(task => { + if (!task) { + const errorMessage = typeof taskId === 'string' + ? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId) + : nls.localize('DebugTaskNotFound', "Could not find the specified task."); + return TPromise.wrapError(errors.create(errorMessage)); + } + + function once(kind: TaskEventKind, event: Event): Event { + return (listener, thisArgs = null, disposables?) => { + const result = event(e => { + if (e.kind === kind) { + result.dispose(); + return listener.call(thisArgs, e); + } + }, null, disposables); + return result; + }; + } + // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 + let taskStarted = false; + const promise = this.taskService.getActiveTasks().then(tasks => { + if (tasks.filter(t => t._id === task._id).length) { + // task is already running - nothing to do. + return TPromise.as(null); + } + once(TaskEventKind.Active, this.taskService.onDidStateChange)((taskEvent) => { + taskStarted = true; + }); + const taskPromise = this.taskService.run(task); + if (task.isBackground) { + return new TPromise((c, e) => once(TaskEventKind.Inactive, this.taskService.onDidStateChange)(() => c(null))); + } + + return taskPromise; + }); + + return new TPromise((c, e) => { + promise.then(result => { + taskStarted = true; + c(result); + }, error => e(error)); + + setTimeout(() => { + if (!taskStarted) { + const errorMessage = typeof taskId === 'string' + ? nls.localize('taskNotTrackedWithTaskId', "The specified task cannot be tracked.") + : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked.", JSON.stringify(taskId)); + e({ severity: severity.Error, message: errorMessage }); + } + }, 10000); + }); + }); + } + + //---- focus management + + tryToAutoFocusStackFrame(thread: IThread): TPromise { const callStack = thread.getCallStack(); if (!callStack.length || (this.viewModel.focusedStackFrame && this.viewModel.focusedStackFrame.thread.getId() === thread.getId())) { return TPromise.as(null); @@ -274,8 +762,8 @@ export class DebugService implements debug.IDebugService { this.focusStackFrame(stackFrameToFocus); if (thread.stoppedDetails) { - if (this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak') { - this.viewletService.openViewlet(debug.VIEWLET_ID).done(undefined, errors.onUnexpectedError); + if (this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak') { + this.viewletService.openViewlet(VIEWLET_ID); } this.windowService.focusWindow(); aria.alert(nls.localize('debuggingPaused', "Debugging paused, reason {0}, {1} {2}", thread.stoppedDetails.reason, stackFrameToFocus.source ? stackFrameToFocus.source.name : '', stackFrameToFocus.range.startLineNumber)); @@ -284,189 +772,281 @@ export class DebugService implements debug.IDebugService { return stackFrameToFocus.openInEditor(this.editorService, true); } - private registerSessionListeners(session: debug.ISession, raw: RawDebugSession): void { - this.toDisposeOnSessionEnd.get(raw.getId()).push(raw); - - this.toDisposeOnSessionEnd.get(raw.getId()).push(raw.onDidInitialize(event => { - aria.status(nls.localize('debuggingStarted', "Debugging started.")); - const sendConfigurationDone = () => { - if (raw && raw.capabilities.supportsConfigurationDoneRequest) { - return raw.configurationDone().done(null, e => { - // Disconnect the debug session on configuration done error #10596 - if (raw) { - raw.disconnect().done(null, errors.onUnexpectedError); - } - this.notificationService.error(e.message); - }); - } - }; - - this.sendAllBreakpoints(session).then(sendConfigurationDone, sendConfigurationDone) - .done(() => this.fetchThreads(raw), errors.onUnexpectedError); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidStop(event => { - this.updateStateAndEmit(session.getId(), debug.State.Stopped); - this.fetchThreads(raw, event.body).done(() => { - const thread = session && session.getThread(event.body.threadId); - if (thread) { - // Call fetch call stack twice, the first only return the top stack frame. - // Second retrieves the rest of the call stack. For performance reasons #25605 - this.model.fetchCallStack(thread).then(() => { - return !event.body.preserveFocusHint ? this.tryToAutoFocusStackFrame(thread) : undefined; - }); - } - }, errors.onUnexpectedError); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidThread(event => { - if (event.body.reason === 'started') { - // debounce to reduce threadsRequest frequency and improve performance - let scheduler = this.fetchThreadsSchedulers.get(session.getId()); - if (!scheduler) { - scheduler = new RunOnceScheduler(() => { - this.fetchThreads(raw).done(undefined, errors.onUnexpectedError); - }, 100); - this.fetchThreadsSchedulers.set(session.getId(), scheduler); - this.toDisposeOnSessionEnd.get(session.getId()).push(scheduler); - } - if (!scheduler.isScheduled()) { - scheduler.schedule(); - } - } else if (event.body.reason === 'exited') { - this.model.clearThreads(session.getId(), true, event.body.threadId); + focusStackFrame(stackFrame: IStackFrame, thread?: IThread, session?: IDebugSession, explicit?: boolean): void { + if (!session) { + if (stackFrame || thread) { + session = stackFrame ? stackFrame.thread.session : thread.session; + } else { + const sessions = this.model.getSessions(); + session = sessions.length ? sessions[0] : undefined; } - })); + } - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidTerminateDebugee(event => { - aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); - if (session && session.getId() === event.sessionId) { - if (event.body && event.body.restart && session) { - this.restartSession(session, event.body.restart).done(null, err => this.notificationService.error(err.message)); - } else { - raw.disconnect().done(null, errors.onUnexpectedError); - } + if (!thread) { + if (stackFrame) { + thread = stackFrame.thread; + } else { + const threads = session ? session.getAllThreads() : undefined; + thread = threads && threads.length ? threads[0] : undefined; } - })); + } - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidContinued(event => { - const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; - this.model.clearThreads(session.getId(), false, threadId); - if (this.viewModel.focusedSession.getId() === session.getId()) { - this.focusStackFrame(undefined, this.viewModel.focusedThread, this.viewModel.focusedSession); + if (!stackFrame) { + if (thread) { + const callStack = thread.getCallStack(); + stackFrame = callStack && callStack.length ? callStack[0] : null; } - this.updateStateAndEmit(session.getId(), debug.State.Running); - })); + } - let outputPromises: TPromise[] = []; - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidOutput(event => { - if (!event.body) { - return; - } - - const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; - if (event.body.category === 'telemetry') { - // only log telemetry events from debug adapter if the debug extension provided the telemetry key - // and the user opted in telemetry - if (raw.customTelemetryService && this.telemetryService.isOptedIn) { - // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. - raw.customTelemetryService.publicLog(event.body.output, event.body.data); - } - - return; - } - - // Make sure to append output in the correct order by properly waiting on preivous promises #33822 - const waitFor = outputPromises.slice(); - const source = event.body.source ? { - lineNumber: event.body.line, - column: event.body.column ? event.body.column : 1, - source: session.getSource(event.body.source) - } : undefined; - if (event.body.variablesReference) { - const container = new ExpressionContainer(session, event.body.variablesReference, generateUuid()); - outputPromises.push(container.getChildren().then(children => { - return TPromise.join(waitFor).then(() => children.forEach(child => { - // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) - child.name = null; - this.logToRepl(child, outputSeverity, source); - })); - })); - } else if (typeof event.body.output === 'string') { - TPromise.join(waitFor).then(() => this.logToRepl(event.body.output, outputSeverity, source)); - } - TPromise.join(outputPromises).then(() => outputPromises = []); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidBreakpoint(event => { - const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; - const breakpoint = this.model.getBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); - const functionBreakpoint = this.model.getFunctionBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); - - if (event.body.reason === 'new' && event.body.breakpoint.source) { - const source = session.getSource(event.body.breakpoint.source); - const bps = this.model.addBreakpoints(source.uri, [{ - column: event.body.breakpoint.column, - enabled: true, - lineNumber: event.body.breakpoint.line, - }], false); - if (bps.length === 1) { - this.model.updateBreakpoints({ [bps[0].getId()]: event.body.breakpoint }); - } - } - - if (event.body.reason === 'removed') { - if (breakpoint) { - this.model.removeBreakpoints([breakpoint]); - } - if (functionBreakpoint) { - this.model.removeFunctionBreakpoints(functionBreakpoint.getId()); - } - } - - if (event.body.reason === 'changed') { - if (breakpoint) { - if (!breakpoint.column) { - event.body.breakpoint.column = undefined; - } - this.model.setBreakpointSessionData(session.getId(), { [breakpoint.getId()]: event.body.breakpoint }); - } - if (functionBreakpoint) { - this.model.setBreakpointSessionData(session.getId(), { [functionBreakpoint.getId()]: event.body.breakpoint }); - } - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidExitAdapter(event => { - // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 - if (strings.equalsIgnoreCase(session.configuration.type, 'extensionhost') && this.sessionStates.get(session.getId()) === debug.State.Running && - session && session.raw.root && session.configuration.noDebug) { - this.broadcastService.broadcast({ - channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, - payload: [session.raw.root.uri.fsPath] - }); - } - if (session && session.getId() === event.sessionId) { - this.onRawSessionEnd(raw); - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidCustomEvent(event => { - this._onDidCustomEvent.fire(event); - })); + this.viewModel.setFocus(stackFrame, thread, session, explicit); } - private fetchThreads(session: RawDebugSession, stoppedDetails?: debug.IRawStoppedDetails): TPromise { - return session.threads().then(response => { - if (response && response.body && response.body.threads) { - response.body.threads.forEach(thread => { - this.model.rawUpdate({ - sessionId: session.getId(), - threadId: thread.id, - thread, - stoppedDetails: stoppedDetails && thread.id === stoppedDetails.threadId ? stoppedDetails : undefined - }); - }); + //---- REPL + + addReplExpression(name: string): TPromise { + return this.model.addReplExpression(this.viewModel.focusedSession, this.viewModel.focusedStackFrame, name) + // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. + .then(() => this.focusStackFrame(this.viewModel.focusedStackFrame, this.viewModel.focusedThread, this.viewModel.focusedSession)); + } + + removeReplExpressions(): void { + this.model.removeReplExpressions(); + } + + private addToRepl(session: IDebugSession, extensionOutput: IRemoteConsoleLog) { + + let sev = extensionOutput.severity === 'warn' ? severity.Warning : extensionOutput.severity === 'error' ? severity.Error : severity.Info; + + const { args, stack } = parse(extensionOutput); + let source: IReplElementSource; + if (stack) { + const frame = getFirstFrame(stack); + if (frame) { + source = { + column: frame.column, + lineNumber: frame.line, + source: session.getSource({ + name: resources.basenameOrAuthority(frame.uri), + path: frame.uri.fsPath + }) + }; + } + } + + // add output for each argument logged + let simpleVals: any[] = []; + for (let i = 0; i < args.length; i++) { + let a = args[i]; + + // undefined gets printed as 'undefined' + if (typeof a === 'undefined') { + simpleVals.push('undefined'); + } + + // null gets printed as 'null' + else if (a === null) { + simpleVals.push('null'); + } + + // objects & arrays are special because we want to inspect them in the REPL + else if (isObject(a) || Array.isArray(a)) { + + // flush any existing simple values logged + if (simpleVals.length) { + this.logToRepl(simpleVals.join(' '), sev, source); + simpleVals = []; + } + + // show object + this.logToRepl(new RawObjectReplElement((a).prototype, a, undefined, nls.localize('snapshotObj', "Only primitive values are shown for this object.")), sev, source); + } + + // string: watch out for % replacement directive + // string substitution and formatting @ https://developer.chrome.com/devtools/docs/console + else if (typeof a === 'string') { + let buf = ''; + + for (let j = 0, len = a.length; j < len; j++) { + if (a[j] === '%' && (a[j + 1] === 's' || a[j + 1] === 'i' || a[j + 1] === 'd' || a[j + 1] === 'O')) { + i++; // read over substitution + buf += !isUndefinedOrNull(args[i]) ? args[i] : ''; // replace + j++; // read over directive + } else { + buf += a[j]; + } + } + + simpleVals.push(buf); + } + + // number or boolean is joined together + else { + simpleVals.push(a); + } + } + + // flush simple values + // always append a new line for output coming from an extension such that separate logs go to separate lines #23695 + if (simpleVals.length) { + this.logToRepl(simpleVals.join(' ') + '\n', sev, source); + } + } + + logToRepl(value: string | IExpression, sev = severity.Info, source?: IReplElementSource): void { + const clearAnsiSequence = '\u001b[2J'; + if (typeof value === 'string' && value.indexOf(clearAnsiSequence) >= 0) { + // [2J is the ansi escape sequence for clearing the display http://ascii-table.com/ansi-escape-sequences.php + this.model.removeReplExpressions(); + this.model.appendToRepl(nls.localize('consoleCleared', "Console was cleared"), severity.Ignore); + value = value.substr(value.lastIndexOf(clearAnsiSequence) + clearAnsiSequence.length); + } + + this.model.appendToRepl(value, sev, source); + } + + //---- watches + + addWatchExpression(name: string): void { + const we = this.model.addWatchExpression(name); + this.viewModel.setSelectedExpression(we); + } + + renameWatchExpression(id: string, newName: string): void { + return this.model.renameWatchExpression(id, newName); + } + + moveWatchExpression(id: string, position: number): void { + this.model.moveWatchExpression(id, position); + } + + removeWatchExpressions(id?: string): void { + this.model.removeWatchExpressions(id); + } + + //---- breakpoints + + enableOrDisableBreakpoints(enable: boolean, breakpoint?: IEnablement): TPromise { + if (breakpoint) { + this.model.setEnablement(breakpoint, enable); + if (breakpoint instanceof Breakpoint) { + return this.sendBreakpoints(breakpoint.uri); + } else if (breakpoint instanceof FunctionBreakpoint) { + return this.sendFunctionBreakpoints(); + } + + return this.sendExceptionBreakpoints(); + } + + this.model.enableOrDisableAllBreakpoints(enable); + return this.sendAllBreakpoints(); + } + + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): TPromise { + const breakpoints = this.model.addBreakpoints(uri, rawBreakpoints); + breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath))); + + return this.sendBreakpoints(uri).then(() => breakpoints); + } + + updateBreakpoints(uri: uri, data: { [id: string]: DebugProtocol.Breakpoint }, sendOnResourceSaved: boolean): void { + this.model.updateBreakpoints(data); + if (sendOnResourceSaved) { + this.breakpointsToSendOnResourceSaved.add(uri.toString()); + } else { + this.sendBreakpoints(uri); + } + } + + removeBreakpoints(id?: string): TPromise { + const toRemove = this.model.getBreakpoints().filter(bp => !id || bp.getId() === id); + toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath))); + const urisToClear = distinct(toRemove, bp => bp.uri.toString()).map(bp => bp.uri); + + this.model.removeBreakpoints(toRemove); + + return TPromise.join(urisToClear.map(uri => this.sendBreakpoints(uri))); + } + + setBreakpointsActivated(activated: boolean): TPromise { + this.model.setBreakpointsActivated(activated); + return this.sendAllBreakpoints(); + } + + addFunctionBreakpoint(name?: string, id?: string): void { + const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); + this.viewModel.setSelectedFunctionBreakpoint(newFunctionBreakpoint); + } + + renameFunctionBreakpoint(id: string, newFunctionName: string): TPromise { + this.model.renameFunctionBreakpoint(id, newFunctionName); + return this.sendFunctionBreakpoints(); + } + + removeFunctionBreakpoints(id?: string): TPromise { + this.model.removeFunctionBreakpoints(id); + return this.sendFunctionBreakpoints(); + } + + sendAllBreakpoints(session?: IDebugSession): TPromise { + return TPromise.join(distinct(this.model.getBreakpoints(), bp => bp.uri.toString()).map(bp => this.sendBreakpoints(bp.uri, false, session))) + .then(() => this.sendFunctionBreakpoints(session)) + // send exception breakpoints at the end since some debug adapters rely on the order + .then(() => this.sendExceptionBreakpoints(session)); + } + + private sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): TPromise { + + const breakpointsToSend = this.model.getBreakpoints({ uri: modelUri, enabledOnly: true }); + + return this.sendToOneOrAllSessions(session, s => { + return s.sendBreakpoints(modelUri, breakpointsToSend, sourceModified).then(data => { + if (data) { + this.model.setBreakpointSessionData(s.getId(), data); + } + }); + }); + } + + private sendFunctionBreakpoints(session?: IDebugSession): TPromise { + + const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); + + return this.sendToOneOrAllSessions(session, s => { + return s.sendFunctionBreakpoints(breakpointsToSend).then(data => { + if (data) { + this.model.setBreakpointSessionData(s.getId(), data); + } + }); + }); + } + + private sendExceptionBreakpoints(session?: IDebugSession): TPromise { + + const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); + + return this.sendToOneOrAllSessions(session, s => { + return s.sendExceptionBreakpoints(enabledExceptionBps); + }); + } + + private sendToOneOrAllSessions(session: IDebugSession, send: (session: IDebugSession) => TPromise): TPromise { + if (session) { + return send(session); + } + return TPromise.join(this.model.getSessions().map(s => send(s))).then(() => void 0); + } + + private onFileChanges(fileChangesEvent: FileChangesEvent): void { + const toRemove = this.model.getBreakpoints().filter(bp => + fileChangesEvent.contains(bp.uri, FileChangeType.DELETED)); + if (toRemove.length) { + this.model.removeBreakpoints(toRemove); + } + + fileChangesEvent.getUpdated().forEach(event => { + + if (this.breakpointsToSendOnResourceSaved.delete(event.resource.toString())) { + this.sendBreakpoints(event.resource, true); } }); } @@ -475,7 +1055,7 @@ export class DebugService implements debug.IDebugService { let result: Breakpoint[]; try { result = JSON.parse(this.storageService.get(DEBUG_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((breakpoint: any) => { - return new Breakpoint(uri.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData); + return new Breakpoint(uri.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData, this.textFileService); }); } catch (e) { } @@ -515,825 +1095,6 @@ export class DebugService implements debug.IDebugService { return result || []; } - public get state(): debug.State { - const focusedThread = this.viewModel.focusedThread; - if (focusedThread && focusedThread.stopped) { - return debug.State.Stopped; - } - const focusedSession = this.viewModel.focusedSession; - if (focusedSession && this.sessionStates.has(focusedSession.getId())) { - return this.sessionStates.get(focusedSession.getId()); - } - if (this.sessionStates.size > 0) { - return debug.State.Initializing; - } - - return debug.State.Inactive; - } - - public get onDidChangeState(): Event { - return this._onDidChangeState.event; - } - - public get onDidNewSession(): Event { - return this._onDidNewSession.event; - } - - public get onDidEndSession(): Event { - return this._onDidEndSession.event; - } - - public get onDidCustomEvent(): Event { - return this._onDidCustomEvent.event; - } - - private updateStateAndEmit(sessionId?: string, newState?: debug.State): void { - if (sessionId) { - if (newState === debug.State.Inactive) { - this.sessionStates.delete(sessionId); - } else { - this.sessionStates.set(sessionId, newState); - } - } - - const state = this.state; - if (this.previousState !== state) { - const stateLabel = debug.State[state]; - if (stateLabel) { - this.debugState.set(stateLabel.toLowerCase()); - } - this.previousState = state; - this._onDidChangeState.fire(state); - } - } - - public focusStackFrame(stackFrame: debug.IStackFrame, thread?: debug.IThread, session?: debug.ISession, explicit?: boolean): void { - if (!session) { - if (stackFrame || thread) { - session = stackFrame ? stackFrame.thread.session : thread.session; - } else { - const sessions = this.model.getSessions(); - session = sessions.length ? sessions[0] : undefined; - } - } - - if (!thread) { - if (stackFrame) { - thread = stackFrame.thread; - } else { - const threads = session ? session.getAllThreads() : undefined; - thread = threads && threads.length ? threads[0] : undefined; - } - } - - if (!stackFrame) { - if (thread) { - const callStack = thread.getCallStack(); - stackFrame = callStack && callStack.length ? callStack[0] : null; - } - } - - this.viewModel.setFocus(stackFrame, thread, session, explicit); - this.updateStateAndEmit(); - } - - public enableOrDisableBreakpoints(enable: boolean, breakpoint?: debug.IEnablement): TPromise { - if (breakpoint) { - this.model.setEnablement(breakpoint, enable); - if (breakpoint instanceof Breakpoint) { - return this.sendBreakpoints(breakpoint.uri); - } else if (breakpoint instanceof FunctionBreakpoint) { - return this.sendFunctionBreakpoints(); - } - - return this.sendExceptionBreakpoints(); - } - - this.model.enableOrDisableAllBreakpoints(enable); - return this.sendAllBreakpoints(); - } - - public addBreakpoints(uri: uri, rawBreakpoints: debug.IBreakpointData[]): TPromise { - const breakpoints = this.model.addBreakpoints(uri, rawBreakpoints); - breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath))); - - return this.sendBreakpoints(uri).then(() => breakpoints); - } - - public updateBreakpoints(uri: uri, data: { [id: string]: DebugProtocol.Breakpoint }, sendOnResourceSaved: boolean): void { - this.model.updateBreakpoints(data); - if (sendOnResourceSaved) { - this.breakpointsToSendOnResourceSaved.add(uri.toString()); - } else { - this.sendBreakpoints(uri); - } - } - - public removeBreakpoints(id?: string): TPromise { - const toRemove = this.model.getBreakpoints().filter(bp => !id || bp.getId() === id); - toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath))); - const urisToClear = distinct(toRemove, bp => bp.uri.toString()).map(bp => bp.uri); - - this.model.removeBreakpoints(toRemove); - - return TPromise.join(urisToClear.map(uri => this.sendBreakpoints(uri))); - } - - public setBreakpointsActivated(activated: boolean): TPromise { - this.model.setBreakpointsActivated(activated); - return this.sendAllBreakpoints(); - } - - public addFunctionBreakpoint(name?: string, id?: string): void { - const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); - this.viewModel.setSelectedFunctionBreakpoint(newFunctionBreakpoint); - } - - public renameFunctionBreakpoint(id: string, newFunctionName: string): TPromise { - this.model.renameFunctionBreakpoint(id, newFunctionName); - return this.sendFunctionBreakpoints(); - } - - public removeFunctionBreakpoints(id?: string): TPromise { - this.model.removeFunctionBreakpoints(id); - return this.sendFunctionBreakpoints(); - } - - public addReplExpression(name: string): TPromise { - return this.model.addReplExpression(this.viewModel.focusedSession, this.viewModel.focusedStackFrame, name) - // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. - .then(() => this.focusStackFrame(this.viewModel.focusedStackFrame, this.viewModel.focusedThread, this.viewModel.focusedSession)); - } - - public removeReplExpressions(): void { - this.model.removeReplExpressions(); - } - - public logToRepl(value: string | debug.IExpression, sev = severity.Info, source?: debug.IReplElementSource): void { - const clearAnsiSequence = '\\u001b[2J'; - if (typeof value === 'string' && value.indexOf(clearAnsiSequence) >= 0) { - // [2J is the ansi escape sequence for clearing the display http://ascii-table.com/ansi-escape-sequences.php - this.model.removeReplExpressions(); - value = value.substr(value.indexOf(clearAnsiSequence) + clearAnsiSequence.length); - } - - this.model.appendToRepl(value, sev, source); - } - - public addWatchExpression(name: string): void { - const we = this.model.addWatchExpression(name); - this.viewModel.setSelectedExpression(we); - } - - public renameWatchExpression(id: string, newName: string): void { - return this.model.renameWatchExpression(id, newName); - } - - public moveWatchExpression(id: string, position: number): void { - this.model.moveWatchExpression(id, position); - } - - public removeWatchExpressions(id?: string): void { - this.model.removeWatchExpressions(id); - } - - public startDebugging(launch: debug.ILaunch, configOrName?: debug.IConfig | string, noDebug = false, unresolvedConfiguration?: debug.IConfig, ): TPromise { - const sessionId = generateUuid(); - this.updateStateAndEmit(sessionId, debug.State.Initializing); - const wrapUpState = () => { - if (this.sessionStates.get(sessionId) === debug.State.Initializing) { - this.updateStateAndEmit(sessionId, debug.State.Inactive); - } - }; - - // make sure to save all files and that the configuration is up to date - return this.extensionService.activateByEvent('onDebug').then(() => this.textFileService.saveAll().then(() => this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined).then(() => - this.extensionService.whenInstalledExtensionsRegistered().then(() => { - if (this.model.getSessions().length === 0) { - this.removeReplExpressions(); - this.allSessions.clear(); - } - - let config: debug.IConfig, compound: debug.ICompound; - if (!configOrName) { - configOrName = this.configurationManager.selectedConfiguration.name; - } - if (typeof configOrName === 'string' && launch) { - config = launch.getConfiguration(configOrName); - compound = launch.getCompound(configOrName); - - const sessions = this.model.getSessions(); - const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(p => p.getName(false) === configOrName && (!launch || !launch.workspace || !p.raw.root || p.raw.root.uri.toString() === launch.workspace.uri.toString()))) { - return TPromise.wrapError(new Error(alreadyRunningMessage)); - } - if (compound && compound.configurations && sessions.some(p => compound.configurations.indexOf(p.getName(false)) !== -1)) { - return TPromise.wrapError(new Error(alreadyRunningMessage)); - } - } else if (typeof configOrName !== 'string') { - config = configOrName; - } - - if (compound) { - if (!compound.configurations) { - return TPromise.wrapError(new Error(nls.localize({ key: 'compoundMustHaveConfigurations', comment: ['compound indicates a "compounds" configuration item', '"configurations" is an attribute and should not be localized'] }, - "Compound must have \"configurations\" attribute set in order to start multiple configurations."))); - } - - return TPromise.join(compound.configurations.map(configData => { - const name = typeof configData === 'string' ? configData : configData.name; - if (name === compound.name) { - return TPromise.as(null); - } - - let launchForName: debug.ILaunch; - if (typeof configData === 'string') { - const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)); - if (launchesContainingName.length === 1) { - launchForName = launchesContainingName[0]; - } else if (launchesContainingName.length > 1 && launchesContainingName.indexOf(launch) >= 0) { - // If there are multiple launches containing the configuration give priority to the configuration in the current launch - launchForName = launch; - } else { - return TPromise.wrapError(new Error(launchesContainingName.length === 0 ? nls.localize('noConfigurationNameInWorkspace', "Could not find launch configuration '{0}' in the workspace.", name) - : nls.localize('multipleConfigurationNamesInWorkspace', "There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name))); - } - } else if (configData.folder) { - const launchesMatchingConfigData = this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.name === configData.folder && !!l.getConfiguration(configData.name)); - if (launchesMatchingConfigData.length === 1) { - launchForName = launchesMatchingConfigData[0]; - } else { - return TPromise.wrapError(new Error(nls.localize('noFolderWithName', "Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", configData.folder, configData.name, compound.name))); - } - } - - return this.startDebugging(launchForName, name, noDebug, unresolvedConfiguration); - })); - } - if (configOrName && !config) { - const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", configOrName) : - nls.localize('launchJsonDoesNotExist', "'launch.json' does not exist."); - return TPromise.wrapError(new Error(message)); - } - - // We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes. - // Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config. - let type: string; - if (config) { - type = config.type; - } else { - // a no-folder workspace has no launch.config - config = {}; - } - unresolvedConfiguration = unresolvedConfiguration || deepClone(config); - - if (noDebug) { - config.noDebug = true; - } - - return (type ? TPromise.as(null) : this.configurationManager.guessDebugger().then(a => type = a && a.type)).then(() => - this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { - // a falsy config indicates an aborted launch - if (config && config.type) { - return this.createSession(launch, config, unresolvedConfiguration, sessionId); - } - - if (launch && type) { - return launch.openConfigFile(false, type).done(undefined, errors.onUnexpectedError); - } - }) - ).then(() => undefined); - }) - ))).then(() => wrapUpState(), err => { - wrapUpState(); - return TPromise.wrapError(err); - }); - } - - private substituteVariables(launch: debug.ILaunch | undefined, config: debug.IConfig): TPromise { - const dbg = this.configurationManager.getDebugger(config.type); - if (dbg) { - let folder: IWorkspaceFolder = undefined; - if (launch && launch.workspace) { - folder = launch.workspace; - } else { - const folders = this.contextService.getWorkspace().folders; - if (folders.length === 1) { - folder = folders[0]; - } - } - return dbg.substituteVariables(folder, config).then(config => { - return config; - }, (err: Error) => { - this.showError(err.message); - return undefined; // bail out - }); - } - return TPromise.as(config); - } - - private createSession(launch: debug.ILaunch, config: debug.IConfig, unresolvedConfig: debug.IConfig, sessionId: string): TPromise { - return this.textFileService.saveAll().then(() => - this.substituteVariables(launch, config).then(resolvedConfig => { - - if (!resolvedConfig) { - // User canceled resolving of interactive variables, silently return - return undefined; - } - - if (!this.configurationManager.getDebugger(resolvedConfig.type) || (config.request !== 'attach' && config.request !== 'launch')) { - let message: string; - if (config.request !== 'attach' && config.request !== 'launch') { - message = config.request ? nls.localize('debugRequestNotSupported', "Attribute '{0}' has an unsupported value '{1}' in the chosen debug configuration.", 'request', config.request) - : nls.localize('debugRequesMissing', "Attribute '{0}' is missing from the chosen debug configuration.", 'request'); - - } else { - message = resolvedConfig.type ? nls.localize('debugTypeNotSupported', "Configured debug type '{0}' is not supported.", resolvedConfig.type) : - nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration."); - } - - return this.showError(message); - } - - this.toDisposeOnSessionEnd.set(sessionId, []); - - const workspace = launch ? launch.workspace : undefined; - const debugAnywayAction = new Action('debug.debugAnyway', nls.localize('debugAnyway', "Debug Anyway"), undefined, true, () => { - return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, sessionId); - }); - - return this.runTask(sessionId, workspace, resolvedConfig.preLaunchTask).then((taskSummary: ITaskSummary) => { - const errorCount = resolvedConfig.preLaunchTask ? this.markerService.getStatistics().errors : 0; - const successExitCode = taskSummary && taskSummary.exitCode === 0; - const failureExitCode = taskSummary && taskSummary.exitCode !== undefined && taskSummary.exitCode !== 0; - if (successExitCode || (errorCount === 0 && !failureExitCode)) { - return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, sessionId); - } - - const message = errorCount > 1 ? nls.localize('preLaunchTaskErrors', "Build errors have been detected during preLaunchTask '{0}'.", resolvedConfig.preLaunchTask) : - errorCount === 1 ? nls.localize('preLaunchTaskError', "Build error has been detected during preLaunchTask '{0}'.", resolvedConfig.preLaunchTask) : - nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", resolvedConfig.preLaunchTask, taskSummary.exitCode); - - const showErrorsAction = new Action('debug.showErrors', nls.localize('showErrors', "Show Errors"), undefined, true, () => { - return this.panelService.openPanel(Constants.MARKERS_PANEL_ID).then(() => undefined); - }); - - return this.showError(message, [debugAnywayAction, showErrorsAction]); - }, (err: TaskError) => { - return this.showError(err.message, [debugAnywayAction, this.taskService.configureAction()]); - }); - }, err => { - if (err && err.message) { - return this.showError(err.message); - } - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved on disk and that you have a debug extension installed for that file type.")); - } - - return launch && launch.openConfigFile(false).then(editor => void 0); - }) - ); - } - - private initializeRawSession(root: IWorkspaceFolder, configuration: { resolved: debug.IConfig, unresolved: debug.IConfig }, sessionId: string, session?: Session): TPromise { - const dbg = this.configurationManager.getDebugger(configuration.resolved.type); - return dbg.getCustomTelemetryService().then(customTelemetryService => { - - const raw = this.instantiationService.createInstance(RawDebugSession, sessionId, configuration.resolved.debugServer, dbg, customTelemetryService, root); - if (!session) { - session = this.model.addSession(configuration, raw); - this.allSessions.set(session.getId(), session); - } else { - session.raw = raw; - } - this.registerSessionListeners(session, raw); - - return raw.initialize({ - clientID: 'vscode', - clientName: product.nameLong, - adapterID: configuration.resolved.type, - pathFormat: 'path', - linesStartAt1: true, - columnsStartAt1: true, - supportsVariableType: true, // #8858 - supportsVariablePaging: true, // #9537 - supportsRunInTerminalRequest: true, // #10574 - locale: platform.locale - }).then((result: DebugProtocol.InitializeResponse) => { - this.model.setExceptionBreakpoints(raw.capabilities.exceptionBreakpointFilters); - return session; - }); - }); - } - - private doCreateSession(root: IWorkspaceFolder, configuration: { resolved: debug.IConfig, unresolved: debug.IConfig }, sessionId: string): TPromise { - - const resolved = configuration.resolved; - resolved.__sessionId = sessionId; - this.inDebugMode.set(true); - - const dbg = this.configurationManager.getDebugger(resolved.type); - return this.initializeRawSession(root, configuration, sessionId).then(session => { - const raw = session.raw; - return (resolved.request === 'attach' ? raw.attach(resolved) : raw.launch(resolved)) - .then((result: DebugProtocol.Response) => { - if (raw.disconnected) { - return TPromise.as(null); - } - this.focusStackFrame(undefined, undefined, session); - this._onDidNewSession.fire(session); - - const internalConsoleOptions = resolved.internalConsoleOptions || this.configurationService.getValue('debug').internalConsoleOptions; - if (internalConsoleOptions === 'openOnSessionStart' || (this.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) { - this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); - } - - const openDebug = this.configurationService.getValue('debug').openDebug; - // Open debug viewlet based on the visibility of the side bar and openDebug setting - if (openDebug === 'openOnSessionStart' || (openDebug === 'openOnFirstSessionStart' && this.firstSessionStart)) { - this.viewletService.openViewlet(debug.VIEWLET_ID); - } - this.firstSessionStart = false; - - this.debugType.set(resolved.type); - if (this.model.getSessions().length > 1) { - this.viewModel.setMultiSessionView(true); - } - this.updateStateAndEmit(raw.getId(), debug.State.Running); - - /* __GDPR__ - "debugSessionStart" : { - "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "exceptionBreakpoints": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "extensionName": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, - "isBuiltin": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true}, - "launchJsonExists": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - return this.telemetryService.publicLog('debugSessionStart', { - type: resolved.type, - breakpointCount: this.model.getBreakpoints().length, - exceptionBreakpoints: this.model.getExceptionBreakpoints(), - watchExpressionsCount: this.model.getWatchExpressions().length, - extensionName: dbg.extensionDescription.id, - isBuiltin: dbg.extensionDescription.isBuiltin, - launchJsonExists: root && !!this.configurationService.getValue('launch', { resource: root.uri }) - }); - }).then(() => session, (error: Error | string) => { - if (errors.isPromiseCanceledError(error)) { - // Do not show 'canceled' error messages to the user #7906 - return TPromise.as(null); - } - - const errorMessage = error instanceof Error ? error.message : error; - /* __GDPR__ - "debugMisconfiguration" : { - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "error": { "classification": "CallstackOrException", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('debugMisconfiguration', { type: resolved ? resolved.type : undefined, error: errorMessage }); - this.updateStateAndEmit(raw.getId(), debug.State.Inactive); - if (!raw.disconnected) { - raw.disconnect().done(null, errors.onUnexpectedError); - } else if (session) { - this.model.removeSession(session.getId()); - } - - // Show the repl if some error got logged there #5870 - if (this.model.getReplElements().length > 0) { - this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); - } - if (this.model.getReplElements().length === 0) { - this.inDebugMode.reset(); - } - - this.showError(errorMessage, errors.isErrorWithActions(error) ? error.actions : []); - return undefined; - }); - }); - } - - private showError(message: string, actions: IAction[] = []): TPromise { - const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); - actions.push(configureAction); - return this.dialogService.show(severity.Error, message, actions.map(a => a.label).concat(nls.localize('cancel', "Cancel")), { cancelId: actions.length }).then(choice => { - if (choice < actions.length) { - return actions[choice].run(); - } - - return TPromise.as(null); - }); - } - - private runTask(sessionId: string, root: IWorkspaceFolder, taskName: string): TPromise { - if (!taskName || this.skipRunningTask) { - this.skipRunningTask = false; - return TPromise.as(null); - } - - // run a task before starting a debug session - return this.taskService.getTask(root, taskName).then(task => { - if (!task) { - return TPromise.wrapError(errors.create(nls.localize('DebugTaskNotFound', "Could not find the task \'{0}\'.", taskName))); - } - - function once(kind: TaskEventKind, event: Event): Event { - return (listener, thisArgs = null, disposables?) => { - const result = event(e => { - if (e.kind === kind) { - result.dispose(); - return listener.call(thisArgs, e); - } - }, null, disposables); - return result; - }; - } - // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 - let taskStarted = false; - const promise = this.taskService.getActiveTasks().then(tasks => { - if (tasks.filter(t => t._id === task._id).length) { - // task is already running - nothing to do. - return TPromise.as(null); - } - this.toDisposeOnSessionEnd.get(sessionId).push( - once(TaskEventKind.Active, this.taskService.onDidStateChange)(() => { - taskStarted = true; - }) - ); - const taskPromise = this.taskService.run(task); - if (task.isBackground) { - return new TPromise((c, e) => this.toDisposeOnSessionEnd.get(sessionId).push( - once(TaskEventKind.Inactive, this.taskService.onDidStateChange)(() => c(null))) - ); - } - - return taskPromise; - }); - - return new TPromise((c, e) => { - promise.then(result => { - taskStarted = true; - c(result); - }, error => e(error)); - - setTimeout(() => { - if (!taskStarted) { - e({ severity: severity.Error, message: nls.localize('taskNotTracked', "The task '{0}' cannot be tracked.", taskName) }); - } - }, 10000); - }); - }); - } - - public sourceIsNotAvailable(uri: uri): void { - this.model.sourceIsNotAvailable(uri); - } - - public restartSession(session: debug.ISession, restartData?: any): TPromise { - return this.textFileService.saveAll().then(() => { - if (session.raw.capabilities.supportsRestartRequest) { - return this.runTask(session.getId(), session.raw.root, session.configuration.postDebugTask) - .then(() => this.runTask(session.getId(), session.raw.root, session.configuration.preLaunchTask)) - .then(() => session.raw.custom('restart', null)); - } - const focusedSession = this.viewModel.focusedSession; - const preserveFocus = focusedSession && session.getId() === focusedSession.getId(); - // Do not run preLaunch and postDebug tasks for automatic restarts - this.skipRunningTask = !!restartData; - - return session.raw.disconnect(true).then(() => { - if (strings.equalsIgnoreCase(session.configuration.type, 'extensionHost') && session.raw.root) { - return this.broadcastService.broadcast({ - channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, - payload: [session.raw.root.uri.fsPath] - }); - } - - return new TPromise((c, e) => { - setTimeout(() => { - // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration - let configToUse = session.configuration; - - const launch = session.raw.root ? this.configurationManager.getLaunch(session.raw.root.uri) : undefined; - const unresolvedConfiguration = (session).unresolvedConfiguration; - if (launch) { - const config = launch.getConfiguration(session.configuration.name); - if (config && !equals(config, unresolvedConfiguration)) { - // Take the type from the session since the debug extension might overwrite it #21316 - configToUse = config; - configToUse.type = session.configuration.type; - configToUse.noDebug = session.configuration.noDebug; - } - } - configToUse.__restart = restartData; - this.skipRunningTask = !!restartData; - this.startDebugging(launch, configToUse, configToUse.noDebug, unresolvedConfiguration).then(() => c(null), err => e(err)); - }, 300); - }); - }).then(() => { - if (preserveFocus) { - // Restart should preserve the focused session - const restartedSession = this.model.getSessions().filter(p => p.configuration.name === session.configuration.name).pop(); - if (restartedSession && restartedSession !== this.viewModel.focusedSession) { - this.focusStackFrame(undefined, undefined, restartedSession); - } - } - }); - }); - } - - public stopSession(session: debug.ISession): TPromise { - if (session) { - return session.raw.disconnect(false, true); - } - - const sessions = this.model.getSessions(); - if (sessions.length) { - return TPromise.join(sessions.map(s => s.raw.disconnect(false, true))); - } - - this.sessionStates.clear(); - this._onDidChangeState.fire(); - return undefined; - } - - private onRawSessionEnd(raw: RawDebugSession): void { - const breakpoints = this.model.getBreakpoints(); - const session = this.model.getSessions().filter(p => p.getId() === raw.getId()).pop(); - /* __GDPR__ - "debugSessionStop" : { - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "sessionLengthInSeconds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('debugSessionStop', { - type: session && session.configuration.type, - success: raw.emittedStopped || breakpoints.length === 0, - sessionLengthInSeconds: raw.getLengthInSeconds(), - breakpointCount: breakpoints.length, - watchExpressionsCount: this.model.getWatchExpressions().length - }); - - this.model.removeSession(raw.getId()); - if (session) { - this._onDidEndSession.fire(session); - if (session.configuration.postDebugTask) { - this.runTask(session.getId(), session.raw.root, session.configuration.postDebugTask).done(undefined, err => - this.notificationService.error(err) - ); - } - } - - lifecycle.dispose(this.toDisposeOnSessionEnd.get(raw.getId())); - const focusedSession = this.viewModel.focusedSession; - if (focusedSession && focusedSession.getId() === raw.getId()) { - this.focusStackFrame(null); - } - this.updateStateAndEmit(raw.getId(), debug.State.Inactive); - - if (this.model.getSessions().length === 0) { - this.inDebugMode.reset(); - this.debugType.reset(); - this.viewModel.setMultiSessionView(false); - - if (this.partService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue('debug').openExplorerOnEnd) { - this.viewletService.openViewlet(EXPLORER_VIEWLET_ID).done(null, errors.onUnexpectedError); - } - } - } - - public getModel(): debug.IModel { - return this.model; - } - - public getViewModel(): debug.IViewModel { - return this.viewModel; - } - - public getConfigurationManager(): debug.IConfigurationManager { - return this.configurationManager; - } - - private sendAllBreakpoints(session?: debug.ISession): TPromise { - return TPromise.join(distinct(this.model.getBreakpoints(), bp => bp.uri.toString()).map(bp => this.sendBreakpoints(bp.uri, false, session))) - .then(() => this.sendFunctionBreakpoints(session)) - // send exception breakpoints at the end since some debug adapters rely on the order - .then(() => this.sendExceptionBreakpoints(session)); - } - - private sendBreakpoints(modelUri: uri, sourceModified = false, session?: debug.ISession): TPromise { - - const sendBreakpointsToSession = (session: debug.ISession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints) { - return TPromise.as(null); - } - - const breakpointsToSend = this.model.getBreakpoints({ uri: modelUri, enabledOnly: true }); - - const source = session.getSourceForUri(modelUri); - let rawSource: DebugProtocol.Source; - if (source) { - rawSource = source.raw; - } else { - const data = Source.getEncodedDebugData(modelUri); - rawSource = { name: data.name, path: data.path, sourceReference: data.sourceReference }; - } - - if (breakpointsToSend.length && !rawSource.adapterData) { - rawSource.adapterData = breakpointsToSend[0].adapterData; - } - // Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959 - rawSource.path = normalizeDriveLetter(rawSource.path); - - return raw.setBreakpoints({ - source: rawSource, - lines: breakpointsToSend.map(bp => bp.lineNumber), - breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, column: bp.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })), - sourceModified - }).then(response => { - if (!response || !response.body) { - return; - } - - const data = Object.create(null); - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; - } - this.model.setBreakpointSessionData(raw.getId(), data); - }); - }; - - return this.sendToOneOrAllSessions(session, sendBreakpointsToSession); - } - - private sendFunctionBreakpoints(session?: debug.ISession): TPromise { - const sendFunctionBreakpointsToSession = (session: debug.ISession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints || !raw.capabilities.supportsFunctionBreakpoints) { - return TPromise.as(null); - } - - const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); - return raw.setFunctionBreakpoints({ breakpoints: breakpointsToSend }).then(response => { - if (!response || !response.body) { - return; - } - - const data = Object.create(null); - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; - } - this.model.setBreakpointSessionData(raw.getId(), data); - }); - }; - - return this.sendToOneOrAllSessions(session, sendFunctionBreakpointsToSession); - } - - private sendExceptionBreakpoints(session?: debug.ISession): TPromise { - const sendExceptionBreakpointsToSession = (session: debug.ISession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints || this.model.getExceptionBreakpoints().length === 0) { - return TPromise.as(null); - } - - const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); - return raw.setExceptionBreakpoints({ filters: enabledExceptionBps.map(exb => exb.filter) }); - }; - - return this.sendToOneOrAllSessions(session, sendExceptionBreakpointsToSession); - } - - private sendToOneOrAllSessions(session: debug.ISession, send: (session: debug.ISession) => TPromise): TPromise { - if (session) { - return send(session); - } - - return TPromise.join(this.model.getSessions().map(s => send(s))).then(() => void 0); - } - - private onFileChanges(fileChangesEvent: FileChangesEvent): void { - const toRemove = this.model.getBreakpoints().filter(bp => - fileChangesEvent.contains(bp.uri, FileChangeType.DELETED)); - if (toRemove.length) { - this.model.removeBreakpoints(toRemove); - } - - fileChangesEvent.getUpdated().forEach(event => { - - if (this.breakpointsToSendOnResourceSaved.delete(event.resource.toString())) { - this.sendBreakpoints(event.resource, true).done(null, errors.onUnexpectedError); - } - }); - } - private store(): void { const breakpoints = this.model.getBreakpoints(); if (breakpoints.length) { @@ -1370,8 +1131,63 @@ export class DebugService implements debug.IDebugService { } } - public dispose(): void { - this.toDisposeOnSessionEnd.forEach(toDispose => lifecycle.dispose(toDispose)); - this.toDispose = lifecycle.dispose(this.toDispose); + //---- telemetry + + private telemetryDebugSessionStart(root: IWorkspaceFolder, type: string, extension: IExtensionDescription): TPromise { + /* __GDPR__ + "debugSessionStart" : { + "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "exceptionBreakpoints": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "extensionName": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "isBuiltin": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true}, + "launchJsonExists": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + return this.telemetryService.publicLog('debugSessionStart', { + type: type, + breakpointCount: this.model.getBreakpoints().length, + exceptionBreakpoints: this.model.getExceptionBreakpoints(), + watchExpressionsCount: this.model.getWatchExpressions().length, + extensionName: extension.id, + isBuiltin: extension.isBuiltin, + launchJsonExists: root && !!this.configurationService.getValue('launch', { resource: root.uri }) + }); + } + + private telemetryDebugSessionStop(session: IDebugSession, adapterExitEvent: AdapterEndEvent): TPromise { + + const breakpoints = this.model.getBreakpoints(); + + /* __GDPR__ + "debugSessionStop" : { + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "sessionLengthInSeconds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + return this.telemetryService.publicLog('debugSessionStop', { + type: session && session.configuration.type, + success: adapterExitEvent.emittedStopped || breakpoints.length === 0, + sessionLengthInSeconds: adapterExitEvent.sessionLengthInSeconds, + breakpointCount: breakpoints.length, + watchExpressionsCount: this.model.getWatchExpressions().length + }); + } + + private telemetryDebugMisconfiguration(debugType: string, message: string): TPromise { + /* __GDPR__ + "debugMisconfiguration" : { + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "error": { "classification": "CallstackOrException", "purpose": "FeatureInsight" } + } + */ + return this.telemetryService.publicLog('debugMisconfiguration', { + type: debugType, + error: message + }); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugSession.ts b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts new file mode 100644 index 00000000000..5e1b8eab69a --- /dev/null +++ b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts @@ -0,0 +1,752 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI as uri } from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; +import * as nls from 'vs/nls'; +import * as platform from 'vs/base/common/platform'; +import severity from 'vs/base/common/severity'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ISuggestion } from 'vs/editor/common/modes'; +import { Position } from 'vs/editor/common/core/position'; +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, ActualBreakpoints, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger } from 'vs/workbench/parts/debug/common/debug'; +import { Source } from 'vs/workbench/parts/debug/common/debugSource'; +import { mixin } from 'vs/base/common/objects'; +import { Thread, ExpressionContainer, Model } from 'vs/workbench/parts/debug/common/debugModel'; +import { RawDebugSession } from 'vs/workbench/parts/debug/electron-browser/rawDebugSession'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import product from 'vs/platform/node/product'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; + +export class DebugSession implements IDebugSession { + + private id: string; + private _raw: RawDebugSession; + private _state: State; + private _nullCapabilities: DebugProtocol.Capabilities; + + private sources = new Map(); + private threads = new Map(); + private rawListeners: IDisposable[] = []; + private fetchThreadsScheduler: RunOnceScheduler; + + private readonly _onDidChangeState = new Emitter(); + private readonly _onDidEndAdapter = new Emitter(); + + private readonly _onDidLoadedSource = new Emitter(); + private readonly _onDidCustomEvent = new Emitter(); + + + constructor( + private _configuration: { resolved: IConfig, unresolved: IConfig }, + public root: IWorkspaceFolder, + private model: Model, + @IInstantiationService private instantiationService: IInstantiationService, + @INotificationService private notificationService: INotificationService, + @IDebugService private debugService: IDebugService, + @ITelemetryService private telemetryService: ITelemetryService, + ) { + this.id = generateUuid(); + this.state = State.Initializing; + this._nullCapabilities = Object.create(null); + } + + getId(): string { + return this.id; + } + + get configuration(): IConfig { + return this._configuration.resolved; + } + + get unresolvedConfiguration(): IConfig { + return this._configuration.unresolved; + } + + getName(includeRoot: boolean): string { + return includeRoot && this.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.configuration.name; + } + + get state(): State { + return this._state; + } + + set state(value: State) { + if (this._state !== value) { + this._state = value; + this._onDidChangeState.fire(value); + } + } + + get capabilities(): DebugProtocol.Capabilities { + return this._raw ? this._raw.capabilities : this._nullCapabilities; + } + + //---- events + + get onDidChangeState(): Event { + return this._onDidChangeState.event; + } + + get onDidEndAdapter(): Event { + return this._onDidEndAdapter.event; + } + + //---- DAP events + + get onDidCustomEvent(): Event { + return this._onDidCustomEvent.event; + } + + get onDidLoadedSource(): Event { + return this._onDidLoadedSource.event; + } + + + //---- DAP requests + + /** + * create and initialize a new debug adapter for this session + */ + initialize(dbgr: IDebugger): TPromise { + + if (this._raw) { + // if there was already a connection make sure to remove old listeners + this.dispose(); // TODO: do not use dispose for this! + } + + return dbgr.getCustomTelemetryService().then(customTelemetryService => { + + this._raw = this.instantiationService.createInstance(RawDebugSession, this._configuration.resolved.debugServer, dbgr, customTelemetryService, this.root); + + this.registerListeners(); + + return this._raw.initialize({ + clientID: 'vscode', + clientName: product.nameLong, + adapterID: this.configuration.type, + pathFormat: 'path', + linesStartAt1: true, + columnsStartAt1: true, + supportsVariableType: true, // #8858 + supportsVariablePaging: true, // #9537 + supportsRunInTerminalRequest: true, // #10574 + locale: platform.locale + }).then(response => { + this.model.addSession(this); + this.state = State.Running; + this.model.setExceptionBreakpoints(this._raw.capabilities.exceptionBreakpointFilters); + }); + }); + } + + /** + * launch or attach to the debuggee + */ + launchOrAttach(config: IConfig): TPromise { + if (this._raw) { + + // __sessionID only used for EH debugging (but we add it always for now...) + config.__sessionId = this.getId(); + + return this._raw.launchOrAttach(config).then(result => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * end the current debug adapter session + */ + terminate(restart = false): TPromise { + if (this._raw) { + if (this._raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { + return this._raw.terminate(restart).then(response => { + return void 0; + }); + } + return this._raw.disconnect(restart).then(response => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * end the current debug adapter session + */ + disconnect(restart = false): TPromise { + if (this._raw) { + return this._raw.disconnect(restart).then(response => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * restart debug adapter session + */ + restart(): TPromise { + if (this._raw) { + return this._raw.restart(); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + sendBreakpoints(modelUri: uri, breakpointsToSend: IBreakpoint[], sourceModified: boolean): TPromise { + + if (!this._raw) { + return TPromise.wrapError(new Error('no debug adapter')); + } + + if (!this._raw.readyForBreakpoints) { + return TPromise.as(undefined); + } + + const source = this.getSourceForUri(modelUri); + let rawSource: DebugProtocol.Source; + if (source) { + rawSource = source.raw; + } else { + const data = Source.getEncodedDebugData(modelUri); + rawSource = { name: data.name, path: data.path, sourceReference: data.sourceReference }; + } + + if (breakpointsToSend.length && !rawSource.adapterData) { + rawSource.adapterData = breakpointsToSend[0].adapterData; + } + // Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959 + rawSource.path = normalizeDriveLetter(rawSource.path); + + return this._raw.setBreakpoints({ + source: rawSource, + lines: breakpointsToSend.map(bp => bp.lineNumber), + breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, column: bp.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })), + sourceModified + }).then(response => { + + if (response && response.body) { + const data: ActualBreakpoints = Object.create(null); + for (let i = 0; i < breakpointsToSend.length; i++) { + data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + } + this.model.setBreakpointSessionData(this.getId(), data); + return data; + } + return undefined; + }); + } + + sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): TPromise { + if (this._raw) { + if (this._raw.readyForBreakpoints) { + return this._raw.setFunctionBreakpoints({ breakpoints: fbpts }).then(response => { + if (response && response.body) { + const data: ActualBreakpoints = Object.create(null); + for (let i = 0; i < fbpts.length; i++) { + data[fbpts[i].getId()] = response.body.breakpoints[i]; + } + return data; + } + return undefined; + }); + } + return TPromise.as(undefined); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise { + if (this._raw) { + if (this._raw.readyForBreakpoints && exbpts.length > 0) { + return this._raw.setExceptionBreakpoints({ filters: exbpts.map(exb => exb.filter) }); + } + return TPromise.as(null); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + customRequest(request: string, args: any): TPromise { + if (this._raw) { + return this._raw.custom(request, args); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stackTrace(threadId: number, startFrame: number, levels: number): TPromise { + if (this._raw) { + return this._raw.stackTrace({ threadId, startFrame, levels }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + exceptionInfo(threadId: number): TPromise { + if (this._raw) { + return this._raw.exceptionInfo({ threadId }).then(response => { + if (response) { + return { + id: response.body.exceptionId, + description: response.body.description, + breakMode: response.body.breakMode, + details: response.body.details + }; + } + return null; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + scopes(frameId: number): TPromise { + if (this._raw) { + return this._raw.scopes({ frameId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise { + if (this._raw) { + return this._raw.variables({ variablesReference, filter, start, count }); + } + return TPromise.as(undefined); + } + + evaluate(expression: string, frameId: number, context?: string): TPromise { + if (this._raw) { + return this._raw.evaluate({ expression, frameId, context }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + restartFrame(frameId: number, threadId: number): TPromise { + if (this._raw) { + return this._raw.restartFrame({ frameId }, threadId); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + next(threadId: number): TPromise { + if (this._raw) { + return this._raw.next({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepIn(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepIn({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepOut(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepOut({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepBack(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepBack({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + continue(threadId: number): TPromise { + if (this._raw) { + return this._raw.continue({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + reverseContinue(threadId: number): TPromise { + if (this._raw) { + return this._raw.reverseContinue({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + pause(threadId: number): TPromise { + if (this._raw) { + return this._raw.pause({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + terminateThreads(threadIds?: number[]): TPromise { + if (this._raw) { + return this._raw.terminateThreads({ threadIds }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + setVariable(variablesReference: number, name: string, value: string): TPromise { + if (this._raw) { + return this._raw.setVariable({ variablesReference, name, value }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + loadSource(resource: uri): TPromise { + + if (!this._raw) { + return TPromise.wrapError(new Error('no debug adapter')); + } + + const source = this.getSourceForUri(resource); + let rawSource: DebugProtocol.Source; + if (source) { + rawSource = source.raw; + } else { + // create a Source + + let sourceRef: number; + if (resource.query) { + const data = Source.getEncodedDebugData(resource); + sourceRef = data.sourceReference; + } + + rawSource = { + path: resource.with({ scheme: '', query: '' }).toString(true), // Remove debug: scheme + sourceReference: sourceRef + }; + } + + return this._raw.source({ sourceReference: rawSource.sourceReference, source: rawSource }); + } + + getLoadedSources(): TPromise { + if (this._raw) { + return this._raw.loadedSources({}).then(response => { + return response.body.sources.map(src => this.getSource(src)); + }, () => { + return []; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { + if (this._raw) { + return this._raw.completions({ + frameId, + text, + column: position.column, + line: position.lineNumber + }).then(response => { + + const result: ISuggestion[] = []; + if (response && response.body && response.body.targets) { + response.body.targets.forEach(item => { + if (item && item.label) { + result.push({ + label: item.label, + insertText: item.text || item.label, + type: item.type, + filterText: item.start && item.length && text.substr(item.start, item.length).concat(item.label), + overwriteBefore: item.length || overwriteBefore + }); + } + }); + } + + return result; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + //---- threads + + getThread(threadId: number): Thread { + return this.threads.get(threadId); + } + + getAllThreads(): IThread[] { + const result: IThread[] = []; + this.threads.forEach(t => result.push(t)); + return result; + } + + clearThreads(removeThreads: boolean, reference: number = undefined): void { + if (reference !== undefined && reference !== null) { + if (this.threads.has(reference)) { + const thread = this.threads.get(reference); + thread.clearCallStack(); + thread.stoppedDetails = undefined; + thread.stopped = false; + + if (removeThreads) { + this.threads.delete(reference); + } + } + } else { + this.threads.forEach(thread => { + thread.clearCallStack(); + thread.stoppedDetails = undefined; + thread.stopped = false; + }); + + if (removeThreads) { + this.threads.clear(); + ExpressionContainer.allValues.clear(); + } + } + } + + rawUpdate(data: IRawModelUpdate): void { + + if (data.thread && !this.threads.has(data.threadId)) { + // A new thread came in, initialize it. + this.threads.set(data.threadId, new Thread(this, data.thread.name, data.thread.id)); + } else if (data.thread && data.thread.name) { + // Just the thread name got updated #18244 + this.threads.get(data.threadId).name = data.thread.name; + } + + if (data.stoppedDetails) { + // Set the availability of the threads' callstacks depending on + // whether the thread is stopped or not + if (data.stoppedDetails.allThreadsStopped) { + this.threads.forEach(thread => { + thread.stoppedDetails = thread.threadId === data.threadId ? data.stoppedDetails : { reason: undefined }; + thread.stopped = true; + thread.clearCallStack(); + }); + } else if (this.threads.has(data.threadId)) { + // One thread is stopped, only update that thread. + const thread = this.threads.get(data.threadId); + thread.stoppedDetails = data.stoppedDetails; + thread.clearCallStack(); + thread.stopped = true; + } + } + } + + private fetchThreads(stoppedDetails?: IRawStoppedDetails): TPromise { + return this._raw.threads().then(response => { + if (response && response.body && response.body.threads) { + response.body.threads.forEach(thread => { + this.model.rawUpdate({ + sessionId: this.getId(), + threadId: thread.id, + thread, + stoppedDetails: stoppedDetails && thread.id === stoppedDetails.threadId ? stoppedDetails : undefined + }); + }); + } + }); + } + + //---- private + + private registerListeners(): void { + + this.rawListeners.push(this._raw.onDidInitialize(() => { + aria.status(nls.localize('debuggingStarted', "Debugging started.")); + const sendConfigurationDone = () => { + if (this._raw && this._raw.capabilities.supportsConfigurationDoneRequest) { + return this._raw.configurationDone().then(null, e => { + // Disconnect the debug session on configuration done error #10596 + if (this._raw) { + this._raw.disconnect(); + } + this.notificationService.error(e.message); + }); + } + + return undefined; + }; + + // Send all breakpoints + this.debugService.sendAllBreakpoints(this).then(sendConfigurationDone, sendConfigurationDone) + .then(() => this.fetchThreads()); + })); + + this.rawListeners.push(this._raw.onDidStop(event => { + this.state = State.Stopped; + this.fetchThreads(event.body).then(() => { + const thread = this.getThread(event.body.threadId); + if (thread) { + // Call fetch call stack twice, the first only return the top stack frame. + // Second retrieves the rest of the call stack. For performance reasons #25605 + this.model.fetchCallStack(thread).then(() => { + return !event.body.preserveFocusHint ? this.debugService.tryToAutoFocusStackFrame(thread) : undefined; + }); + } + }); + })); + + this.rawListeners.push(this._raw.onDidThread(event => { + if (event.body.reason === 'started') { + // debounce to reduce threadsRequest frequency and improve performance + if (!this.fetchThreadsScheduler) { + this.fetchThreadsScheduler = new RunOnceScheduler(() => { + this.fetchThreads(); + }, 100); + this.rawListeners.push(this.fetchThreadsScheduler); + } + if (!this.fetchThreadsScheduler.isScheduled()) { + this.fetchThreadsScheduler.schedule(); + } + } else if (event.body.reason === 'exited') { + this.model.clearThreads(this.getId(), true, event.body.threadId); + } + })); + + this.rawListeners.push(this._raw.onDidTerminateDebugee(event => { + aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); + if (event.body && event.body.restart) { + this.debugService.restartSession(this, event.body.restart).then(null, err => this.notificationService.error(err.message)); + } else { + this._raw.disconnect(); + } + })); + + this.rawListeners.push(this._raw.onDidContinued(event => { + const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; + this.model.clearThreads(this.getId(), false, threadId); + this.state = State.Running; + })); + + let outputPromises: TPromise[] = []; + this.rawListeners.push(this._raw.onDidOutput(event => { + if (!event.body) { + return; + } + + const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; + if (event.body.category === 'telemetry') { + // only log telemetry events from debug adapter if the debug extension provided the telemetry key + // and the user opted in telemetry + if (this._raw.customTelemetryService && this.telemetryService.isOptedIn) { + // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. + this._raw.customTelemetryService.publicLog(event.body.output, event.body.data); + } + + return; + } + + // Make sure to append output in the correct order by properly waiting on preivous promises #33822 + const waitFor = outputPromises.slice(); + const source = event.body.source ? { + lineNumber: event.body.line, + column: event.body.column ? event.body.column : 1, + source: this.getSource(event.body.source) + } : undefined; + if (event.body.variablesReference) { + const container = new ExpressionContainer(this, event.body.variablesReference, generateUuid()); + outputPromises.push(container.getChildren().then(children => { + return TPromise.join(waitFor).then(() => children.forEach(child => { + // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) + child.name = null; + this.debugService.logToRepl(child, outputSeverity, source); + })); + })); + } else if (typeof event.body.output === 'string') { + TPromise.join(waitFor).then(() => this.debugService.logToRepl(event.body.output, outputSeverity, source)); + } + TPromise.join(outputPromises).then(() => outputPromises = []); + })); + + this.rawListeners.push(this._raw.onDidBreakpoint(event => { + const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; + const breakpoint = this.model.getBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); + const functionBreakpoint = this.model.getFunctionBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); + + if (event.body.reason === 'new' && event.body.breakpoint.source) { + const source = this.getSource(event.body.breakpoint.source); + const bps = this.model.addBreakpoints(source.uri, [{ + column: event.body.breakpoint.column, + enabled: true, + lineNumber: event.body.breakpoint.line, + }], false); + if (bps.length === 1) { + this.model.updateBreakpoints({ [bps[0].getId()]: event.body.breakpoint }); + } + } + + if (event.body.reason === 'removed') { + if (breakpoint) { + this.model.removeBreakpoints([breakpoint]); + } + if (functionBreakpoint) { + this.model.removeFunctionBreakpoints(functionBreakpoint.getId()); + } + } + + if (event.body.reason === 'changed') { + if (breakpoint) { + if (!breakpoint.column) { + event.body.breakpoint.column = undefined; + } + this.model.setBreakpointSessionData(this.getId(), { [breakpoint.getId()]: event.body.breakpoint }); + } + if (functionBreakpoint) { + this.model.setBreakpointSessionData(this.getId(), { [functionBreakpoint.getId()]: event.body.breakpoint }); + } + } + })); + + this.rawListeners.push(this._raw.onDidLoadedSource(event => { + this._onDidLoadedSource.fire({ + reason: event.body.reason, + source: this.getSource(event.body.source) + }); + })); + + this.rawListeners.push(this._raw.onDidCustomEvent(event => { + this._onDidCustomEvent.fire(event); + })); + + this.rawListeners.push(this._raw.onDidExitAdapter(event => { + this._onDidEndAdapter.fire(event); + })); + } + + dispose(): void { + dispose(this.rawListeners); + this.model.clearThreads(this.getId(), true); + this.model.removeSession(this.getId()); + this.fetchThreadsScheduler = undefined; + if (this._raw) { + this._raw.disconnect(); + } + this._raw = undefined; + } + + //---- sources + + getSourceForUri(modelUri: uri): Source { + return this.sources.get(modelUri.toString()); + } + + getSource(raw: DebugProtocol.Source): Source { + let source = new Source(raw, this.getId()); + if (this.sources.has(source.uri.toString())) { + source = this.sources.get(source.uri.toString()); + source.raw = mixin(source.raw, raw); + if (source.raw && raw) { + // Always take the latest presentation hint from adapter #42139 + source.raw.presentationHint = raw.presentationHint; + } + } else { + this.sources.set(source.uri.toString(), source); + } + + return source; + } +} diff --git a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts index c7745f86c64..ca825394e33 100644 --- a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts +++ b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts @@ -26,7 +26,7 @@ export class CopyValueAction extends Action { if (this.value instanceof Variable) { const frameId = this.debugService.getViewModel().focusedStackFrame.frameId; const session = this.debugService.getViewModel().focusedSession; - return session.raw.evaluate({ expression: this.value.evaluateName, frameId }).then(result => { + return session.evaluate(this.value.evaluateName, frameId).then(result => { clipboard.writeText(result.body.result); }, err => clipboard.writeText(this.value.value)); } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 645efdd8418..473552f09e4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -10,87 +10,93 @@ import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import * as debug from 'vs/workbench/parts/debug/common/debug'; -import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { formatPII } from 'vs/workbench/parts/debug/common/debugUtils'; -import { SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/parts/debug/common/debug'; +export class RawDebugSession { -export interface SessionExitedEvent extends debug.DebugEvent { - body: { - exitCode: number, - sessionId: string - }; -} - -export interface SessionTerminatedEvent extends debug.DebugEvent { - body: { - restart?: boolean, - sessionId: string - }; -} - -export class RawDebugSession implements debug.IRawSession { - - private debugAdapter: debug.IDebugAdapter; - - public emittedStopped: boolean; - public readyForBreakpoints: boolean; - - private cachedInitServerP: TPromise; - private startTime: number; - public disconnected: boolean; - private sentPromises: TPromise[]; - private _capabilities: DebugProtocol.Capabilities; + private debugAdapter: IDebugAdapter; + private cachedInitDebugAdapterP: TPromise; private allThreadsContinued: boolean; + private _readyForBreakpoints: boolean; + private _capabilities: DebugProtocol.Capabilities; + // shutdown + private inShutdown: boolean; + private terminated: boolean; + private firedAdapterExitEvent: boolean; + + // telemetry + private startTime: number; + private emittedStopped: boolean; + + // DAP events private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; private readonly _onDidContinued: Emitter; - private readonly _onDidTerminateDebugee: Emitter; + private readonly _onDidTerminateDebugee: Emitter; private readonly _onDidExitDebugee: Emitter; - private readonly _onDidExitAdapter: Emitter<{ sessionId: string }>; private readonly _onDidThread: Emitter; private readonly _onDidOutput: Emitter; private readonly _onDidBreakpoint: Emitter; - private readonly _onDidCustomEvent: Emitter; + private readonly _onDidLoadedSource: Emitter; + private readonly _onDidCustomEvent: Emitter; private readonly _onDidEvent: Emitter; + // DA events + private readonly _onDidExitAdapter: Emitter; + constructor( - private id: string, private debugServerPort: number, - private _debugger: Debugger, + private _debugger: IDebugger, public customTelemetryService: ITelemetryService, - public root: IWorkspaceFolder, - @INotificationService private notificationService: INotificationService, + private root: IWorkspaceFolder, @ITelemetryService private telemetryService: ITelemetryService, @IOutputService private outputService: IOutputService ) { + this._readyForBreakpoints = false; + this.inShutdown = false; + this.firedAdapterExitEvent = false; + this.emittedStopped = false; - this.readyForBreakpoints = false; + this.allThreadsContinued = true; - this.sentPromises = []; this._onDidInitialize = new Emitter(); this._onDidStop = new Emitter(); this._onDidContinued = new Emitter(); - this._onDidTerminateDebugee = new Emitter(); + this._onDidTerminateDebugee = new Emitter(); this._onDidExitDebugee = new Emitter(); - this._onDidExitAdapter = new Emitter<{ sessionId: string }>(); this._onDidThread = new Emitter(); this._onDidOutput = new Emitter(); this._onDidBreakpoint = new Emitter(); - this._onDidCustomEvent = new Emitter(); + this._onDidLoadedSource = new Emitter(); + this._onDidCustomEvent = new Emitter(); this._onDidEvent = new Emitter(); + + this._onDidExitAdapter = new Emitter(); } - public getId(): string { - return this.id; + public get onDidExitAdapter(): Event { + return this._onDidExitAdapter.event; } + public get capabilities(): DebugProtocol.Capabilities { + return this._capabilities || {}; + } + + /** + * DA is ready to accepts setBreakpoint requests. + * Becomes true after "initialized" events has been received. + */ + public get readyForBreakpoints(): boolean { + return this._readyForBreakpoints; + } + + //---- DAP events + public get onDidInitialize(): Event { return this._onDidInitialize.event; } @@ -103,7 +109,7 @@ export class RawDebugSession implements debug.IRawSession { return this._onDidContinued.event; } - public get onDidTerminateDebugee(): Event { + public get onDidTerminateDebugee(): Event { return this._onDidTerminateDebugee.event; } @@ -111,10 +117,6 @@ export class RawDebugSession implements debug.IRawSession { return this._onDidExitDebugee.event; } - public get onDidExitAdapter(): Event<{ sessionId: string }> { - return this._onDidExitAdapter.event; - } - public get onDidThread(): Event { return this._onDidThread.event; } @@ -127,7 +129,11 @@ export class RawDebugSession implements debug.IRawSession { return this._onDidBreakpoint.event; } - public get onDidCustomEvent(): Event { + public get onDidLoadedSource(): Event { + return this._onDidLoadedSource.event; + } + + public get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } @@ -135,173 +141,46 @@ export class RawDebugSession implements debug.IRawSession { return this._onDidEvent.event; } - private initServer(): TPromise { - - if (this.cachedInitServerP) { - return this.cachedInitServerP; - } - - const startSessionP = this.startSession(); - - this.cachedInitServerP = startSessionP.then(() => { - this.startTime = new Date().getTime(); - }, err => { - this.cachedInitServerP = null; - return TPromise.wrapError(err); - }); - - return this.cachedInitServerP; - } - - private startSession(): TPromise { - - return this._debugger.createDebugAdapter(this.root, this.outputService, this.debugServerPort).then(debugAdapter => { - - this.debugAdapter = debugAdapter; - - this.debugAdapter.onError(err => this.onDebugAdapterError(err)); - this.debugAdapter.onEvent(event => this.onDapEvent(event)); - this.debugAdapter.onRequest(request => this.dispatchRequest(request)); - this.debugAdapter.onExit(code => this.onDebugAdapterExit()); - - return this.debugAdapter.startSession(); - }); - } - - public custom(request: string, args: any): TPromise { - return this.send(request, args); - } - - private send(command: string, args: any, cancelOnDisconnect = true): TPromise { - return this.initServer().then(() => { - const promise = this.internalSend(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => { - const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; - const errorMessage = errorResponse ? errorResponse.message : ''; - const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; - if (error && error.sendTelemetry) { - /* __GDPR__ - "debugProtocolErrorResponse" : { - "error" : { "classification": "CallstackOrException", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); - if (this.customTelemetryService) { - /* __GDPR__TODO__ - The message is sent in the name of the adapter but the adapter doesn't know about it. - However, since adapters are an open-ended set, we can not declared the events statically either. - */ - this.customTelemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); - } - } - - const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage; - if (error && error.url) { - const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); - return TPromise.wrapError(errors.create(userMessage, { - actions: [new Action('debug.moreInfo', label, null, true, () => { - window.open(error.url); - return TPromise.as(null); - })] - })); - } - - return errors.isPromiseCanceledError(errorResponse) ? undefined : TPromise.wrapError(new Error(userMessage)); - }); - - if (cancelOnDisconnect) { - this.sentPromises.push(promise); - } - return promise; - }); - } - - private internalSend(command: string, args: any): TPromise { - let errorCallback: (error: Error) => void; - return new TPromise((completeDispatch, errorDispatch) => { - errorCallback = errorDispatch; - this.debugAdapter.sendRequest(command, args, (result: R) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } - }); - }, () => errorCallback(errors.canceled())); - } - - private onDapEvent(event: debug.DebugEvent): void { - event.sessionId = this.id; - - if (event.event === 'initialized') { - this.readyForBreakpoints = true; - this._onDidInitialize.fire(event); - } else if (event.event === 'capabilities' && event.body) { - const capabilites = (event).body.capabilities; - this._capabilities = objects.mixin(this._capabilities, capabilites); - } else if (event.event === 'stopped') { - this.emittedStopped = true; - this._onDidStop.fire(event); - } else if (event.event === 'continued') { - this.allThreadsContinued = (event).body.allThreadsContinued === false ? false : true; - this._onDidContinued.fire(event); - } else if (event.event === 'thread') { - this._onDidThread.fire(event); - } else if (event.event === 'output') { - this._onDidOutput.fire(event); - } else if (event.event === 'breakpoint') { - this._onDidBreakpoint.fire(event); - } else if (event.event === 'terminated') { - this._onDidTerminateDebugee.fire(event); - } else if (event.event === 'exit') { - this._onDidExitDebugee.fire(event); - } else { - this._onDidCustomEvent.fire(event); - } - - this._onDidEvent.fire(event); - } - - public get capabilities(): DebugProtocol.Capabilities { - return this._capabilities || {}; - } + //---- DAP requests public initialize(args: DebugProtocol.InitializeRequestArguments): TPromise { - return this.send('initialize', args).then(response => this.readCapabilities(response)); + return this.send('initialize', args).then((response: DebugProtocol.InitializeResponse) => { + this.mergeCapabilities(response.body); + return response; + }); } - private readCapabilities(response: DebugProtocol.Response): DebugProtocol.Response { - if (response) { - this._capabilities = objects.mixin(this._capabilities, response.body); + public launchOrAttach(config: IConfig): TPromise { + return this.send(config.request, config).then(response => { + this.mergeCapabilities(response.body); + return response; + }); + } + + public restart(): TPromise { + if (this.capabilities.supportsRestartRequest) { + return this.send('restart', null); } - - return response; - } - - public launch(args: DebugProtocol.LaunchRequestArguments): TPromise { - return this.send('launch', args).then(response => this.readCapabilities(response)); - } - - public attach(args: DebugProtocol.AttachRequestArguments): TPromise { - return this.send('attach', args).then(response => this.readCapabilities(response)); + return TPromise.wrapError(new Error('restart not supported')); } public next(args: DebugProtocol.NextArguments): TPromise { return this.send('next', args).then(response => { - this.fireFakeContinued(args.threadId); + this.fireSimulatedContinuedEvent(args.threadId); return response; }); } public stepIn(args: DebugProtocol.StepInArguments): TPromise { return this.send('stepIn', args).then(response => { - this.fireFakeContinued(args.threadId); + this.fireSimulatedContinuedEvent(args.threadId); return response; }); } public stepOut(args: DebugProtocol.StepOutArguments): TPromise { return this.send('stepOut', args).then(response => { - this.fireFakeContinued(args.threadId); + this.fireSimulatedContinuedEvent(args.threadId); return response; }); } @@ -311,7 +190,7 @@ export class RawDebugSession implements debug.IRawSession { if (response && response.body && response.body.allThreadsContinued !== undefined) { this.allThreadsContinued = response.body.allThreadsContinued; } - this.fireFakeContinued(args.threadId, this.allThreadsContinued); + this.fireSimulatedContinuedEvent(args.threadId, this.allThreadsContinued); return response; }); } @@ -330,34 +209,37 @@ export class RawDebugSession implements debug.IRawSession { public restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise { return this.send('restartFrame', args).then(response => { - this.fireFakeContinued(threadId); + this.fireSimulatedContinuedEvent(threadId); return response; }); } public completions(args: DebugProtocol.CompletionsArguments): TPromise { - return this.send('completions', args); + if (this.capabilities.supportsCompletionsRequest) { + return this.send('completions', args); + } + return TPromise.wrapError(new Error('completions not supported')); } - public disconnect(restart = false, force = false): TPromise { - if (this.disconnected && force) { - return this.stopServer(); + /** + * Try terminate the debuggee softly + */ + public terminate(restart = false): TPromise { + if (this.capabilities.supportsTerminateRequest) { + if (!this.terminated) { + this.terminated = true; + return this.send('terminate', { restart }); + } + return this.disconnect(restart); } + return TPromise.wrapError(new Error('terminated not supported')); + } - // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. - // Give a 1s timeout to give a chance for some promises to complete. - setTimeout(() => { - this.sentPromises.forEach(p => p && p.cancel()); - this.sentPromises = []; - }, 1000); - - if (this.debugAdapter && !this.disconnected) { - // point of no return: from now on don't report any errors - this.disconnected = true; - return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer()); - } - - return TPromise.as(null); + /** + * Terminate the debuggee + */ + public disconnect(restart = false): TPromise { + return this.shutdown(undefined, restart); } public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise { @@ -365,7 +247,10 @@ export class RawDebugSession implements debug.IRawSession { } public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): TPromise { - return this.send('setFunctionBreakpoints', args); + if (this.capabilities.supportsFunctionBreakpoints) { + return this.send('setFunctionBreakpoints', args); + } + return TPromise.wrapError(new Error('setFunctionBreakpoints not supported')); } public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): TPromise { @@ -396,6 +281,13 @@ export class RawDebugSession implements debug.IRawSession { return this.send('source', args); } + public loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise { + if (this.capabilities.supportsLoadedSourcesRequest) { + return this.send('loadedSources', args); + } + return TPromise.wrapError(new Error('loadedSources not supported')); + } + public threads(): TPromise { return this.send('threads', null); } @@ -407,7 +299,7 @@ export class RawDebugSession implements debug.IRawSession { public stepBack(args: DebugProtocol.StepBackArguments): TPromise { return this.send('stepBack', args).then(response => { if (response.body === undefined) { - this.fireFakeContinued(args.threadId); + this.fireSimulatedContinuedEvent(args.threadId); } return response; }); @@ -416,14 +308,145 @@ export class RawDebugSession implements debug.IRawSession { public reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise { return this.send('reverseContinue', args).then(response => { if (response.body === undefined) { - this.fireFakeContinued(args.threadId); + this.fireSimulatedContinuedEvent(args.threadId); } return response; }); } - public getLengthInSeconds(): number { - return (new Date().getTime() - this.startTime) / 1000; + public custom(request: string, args: any): TPromise { + return this.send(request, args); + } + + //---- private + + private startAdapter(): TPromise { + + if (!this.cachedInitDebugAdapterP) { + + const startSessionP = this._debugger.createDebugAdapter(this.root, this.outputService, this.debugServerPort).then(debugAdapter => { + + this.debugAdapter = debugAdapter; + + this.debugAdapter.onError(err => { + this.shutdown(err); + }); + + this.debugAdapter.onExit(code => { + if (code !== 0) { + this.shutdown(new Error(`exit code: ${code}`)); + } else { + // normal exit + this.shutdown(); + } + }); + + this.debugAdapter.onEvent(event => this.onDapEvent(event)); + this.debugAdapter.onRequest(request => this.dispatchRequest(request)); + + return this.debugAdapter.startSession(); + }); + + this.cachedInitDebugAdapterP = startSessionP.then(() => { + this.startTime = new Date().getTime(); + }, err => { + return TPromise.wrapError(err); + }); + } + + return this.cachedInitDebugAdapterP; + } + + private shutdown(error?: Error, restart = false): TPromise { + if (!this.inShutdown) { + this.inShutdown = true; + if (this.debugAdapter) { + return this.send('disconnect', { restart }, 500).then(() => { + this.stopAdapter(error); + }, () => { + // ignore error + this.stopAdapter(error); + }); + } + return this.stopAdapter(error); + } + return TPromise.as(undefined); + } + + private stopAdapter(error?: Error): TPromise { + if (this.debugAdapter) { + const da = this.debugAdapter; + this.debugAdapter = null; + return da.stopSession().then(_ => { + this.fireAdapterExitEvent(error); + }, err => { + this.fireAdapterExitEvent(error); + }); + } else { + this.fireAdapterExitEvent(error); + } + return TPromise.as(undefined); + } + + private fireAdapterExitEvent(error?: Error): void { + if (!this.firedAdapterExitEvent) { + this.firedAdapterExitEvent = true; + + const e: AdapterEndEvent = { + emittedStopped: this.emittedStopped, + sessionLengthInSeconds: (new Date().getTime() - this.startTime) / 1000 + }; + if (error) { + e.error = error; + } + this._onDidExitAdapter.fire(e); + } + } + + private onDapEvent(event: DebugProtocol.Event): void { + + switch (event.event) { + case 'initialized': + this._readyForBreakpoints = true; + this._onDidInitialize.fire(event); + break; + case 'loadedSource': + this._onDidLoadedSource.fire(event); + break; + case 'capabilities': + if (event.body) { + const capabilites = (event).body.capabilities; + this.mergeCapabilities(capabilites); + } + break; + case 'stopped': + this.emittedStopped = true; + this._onDidStop.fire(event); + break; + case 'continued': + this.allThreadsContinued = (event).body.allThreadsContinued === false ? false : true; + this._onDidContinued.fire(event); + break; + case 'thread': + this._onDidThread.fire(event); + break; + case 'output': + this._onDidOutput.fire(event); + break; + case 'breakpoint': + this._onDidBreakpoint.fire(event); + break; + case 'terminated': + this._onDidTerminateDebugee.fire(event); + break; + case 'exit': + this._onDidExitDebugee.fire(event); + break; + default: + this._onDidCustomEvent.fire(event); + break; + } + this._onDidEvent.fire(event); } private dispatchRequest(request: DebugProtocol.Request): void { @@ -436,39 +459,93 @@ export class RawDebugSession implements debug.IRawSession { success: true }; - if (request.command === 'runInTerminal') { + const safeSendResponse = (response) => this.debugAdapter && this.debugAdapter.sendResponse(response); - this._debugger.runInTerminal(request.arguments).then(_ => { - response.body = {}; - this.debugAdapter.sendResponse(response); - }, err => { + switch (request.command) { + case 'runInTerminal': + this._debugger.runInTerminal(request.arguments).then(_ => { + response.body = {}; + safeSendResponse(response); + }, err => { + response.success = false; + response.message = err.message; + safeSendResponse(response); + }); + break; + case 'handshake': + try { + const vsda = require.__$__nodeRequire('vsda'); + const obj = new vsda.signer(); + const sig = obj.sign(request.arguments.value); + response.body = { + signature: sig + }; + safeSendResponse(response); + } catch (e) { + response.success = false; + response.message = e.message; + safeSendResponse(response); + } + break; + default: response.success = false; - response.message = err.message; - this.debugAdapter.sendResponse(response); - }); - - } else if (request.command === 'handshake') { - try { - const vsda = require.__$__nodeRequire('vsda'); - const obj = new vsda.signer(); - const sig = obj.sign(request.arguments.value); - response.body = { - signature: sig - }; - this.debugAdapter.sendResponse(response); - } catch (e) { - response.success = false; - response.message = e.message; - this.debugAdapter.sendResponse(response); - } - } else { - response.success = false; - response.message = `unknown request '${request.command}'`; - this.debugAdapter.sendResponse(response); + response.message = `unknown request '${request.command}'`; + safeSendResponse(response); + break; } } - private fireFakeContinued(threadId: number, allThreadsContinued = false): void { + private send(command: string, args: any, timeout?: number): TPromise { + + return this.startAdapter().then(() => { + + return new TPromise((completeDispatch, errorDispatch) => { + this.debugAdapter.sendRequest(command, args, (response: R) => { + if (response.success) { + completeDispatch(response); + } else { + errorDispatch(response); + } + }, timeout); + }).then(response => response, err => TPromise.wrapError(this.handleErrorResponse(err))); + }); + } + + private handleErrorResponse(errorResponse: DebugProtocol.Response): Error { + + if (errorResponse.command === 'canceled' && errorResponse.message === 'canceled') { + return errors.canceled(); + } + + const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; + const errorMessage = errorResponse ? errorResponse.message : ''; + + if (error && error.sendTelemetry) { + const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; + this.telemetryDebugProtocolErrorResponse(telemetryMessage); + } + + const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage; + if (error && error.url) { + const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); + return errors.create(userMessage, { + actions: [new Action('debug.moreInfo', label, null, true, () => { + window.open(error.url); + return TPromise.as(null); + })] + }); + } + + return new Error(userMessage); + } + + private mergeCapabilities(capabilities: DebugProtocol.Capabilities): void { + if (capabilities) { + this._capabilities = objects.mixin(this._capabilities, capabilities); + } + } + + private fireSimulatedContinuedEvent(threadId: number, allThreadsContinued = false): void { this._onDidContinued.fire({ type: 'event', event: 'continued', @@ -480,38 +557,19 @@ export class RawDebugSession implements debug.IRawSession { }); } - private stopServer(): TPromise { - - if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { - this.debugAdapter.stopSession(); - this.cachedInitServerP = null; + private telemetryDebugProtocolErrorResponse(telemetryMessage: string) { + /* __GDPR__ + "debugProtocolErrorResponse" : { + "error" : { "classification": "CallstackOrException", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); + if (this.customTelemetryService) { + /* __GDPR__TODO__ + The message is sent in the name of the adapter but the adapter doesn't know about it. + However, since adapters are an open-ended set, we can not declared the events statically either. + */ + this.customTelemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); } - - this._onDidExitAdapter.fire({ sessionId: this.getId() }); - this.disconnected = true; - if (!this.debugAdapter || this.debugAdapter instanceof SocketDebugAdapter) { - return TPromise.as(null); - } - - - return this.debugAdapter.stopSession(); - } - - private onDebugAdapterError(err: Error): void { - this.notificationService.error(err.message || err.toString()); - this.stopServer().done(null, errors.onUnexpectedError); - } - - private onDebugAdapterExit(): void { - this.debugAdapter = null; - this.cachedInitServerP = null; - if (!this.disconnected) { - this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); - } - this._onDidExitAdapter.fire({ sessionId: this.getId() }); - } - - public dispose(): void { - this.disconnect().done(null, errors.onUnexpectedError); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index b1cc5998947..9ebd5e49f77 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -5,12 +5,12 @@ import 'vs/css!vs/workbench/parts/debug/browser/media/repl'; import * as nls from 'vs/nls'; -import uri from 'vs/base/common/uri'; -import { wireCancellationToken } from 'vs/base/common/async'; +import { URI as uri } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; import { IAction } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; +import * as aria from 'vs/base/browser/ui/aria/aria'; import { isMacintosh } from 'vs/base/common/platform'; import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -24,14 +24,12 @@ import { registerEditorAction, ServicesAccessor, EditorAction, EditorCommand, re import { IModelService } from 'vs/editor/common/services/modelService'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ReplExpressionsRenderer, ReplExpressionsController, ReplExpressionsDataSource, ReplExpressionsActionProvider, ReplExpressionsAccessibilityProvider } from 'vs/workbench/parts/debug/electron-browser/replViewer'; -import { SimpleDebugEditor } from 'vs/workbench/parts/debug/electron-browser/simpleDebugEditor'; import { ClearReplAction } from 'vs/workbench/parts/debug/browser/debugActions'; -import { ReplHistory } from 'vs/workbench/parts/debug/common/replHistory'; import { Panel } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -43,7 +41,13 @@ import { dispose } from 'vs/base/common/lifecycle'; import { OpenMode, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_ON_FIRST_DEBUG_REPL_LINE, CONTEXT_IN_DEBUG_REPL, CONTEXT_ON_LAST_DEBUG_REPL_LINE } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL } from 'vs/workbench/parts/debug/common/debug'; +import { HistoryNavigator } from 'vs/base/common/history'; +import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; @@ -57,17 +61,16 @@ const IPrivateReplService = createDecorator('privateReplSer export interface IPrivateReplService { _serviceBrand: any; - navigateHistory(previous: boolean): void; acceptReplInput(): void; getVisibleContent(): string; } -export class Repl extends Panel implements IPrivateReplService { +export class Repl extends Panel implements IPrivateReplService, IHistoryNavigationWidget { public _serviceBrand: any; private static readonly HALF_WIDTH_TYPICAL = 'n'; - private static HISTORY: ReplHistory; + private history: HistoryNavigator; private static readonly REFRESH_DELAY = 500; // delay in ms to refresh the repl for new elements to show private static readonly REPL_INPUT_INITIAL_HEIGHT = 19; private static readonly REPL_INPUT_MAX_HEIGHT = 170; @@ -83,6 +86,7 @@ export class Repl extends Panel implements IPrivateReplService { private dimension: dom.Dimension; private replInputHeight: number; private model: ITextModel; + private historyNavigationEnablement: IContextKey; constructor( @IDebugService private debugService: IDebugService, @@ -97,14 +101,15 @@ export class Repl extends Panel implements IPrivateReplService { super(REPL_ID, telemetryService, themeService); this.replInputHeight = Repl.REPL_INPUT_INITIAL_HEIGHT; + this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.registerListeners(); } private registerListeners(): void { - this.toUnbind.push(this.debugService.getModel().onDidChangeReplElements(() => { + this._register(this.debugService.getModel().onDidChangeReplElements(() => { this.refreshReplElements(this.debugService.getModel().getReplElements().length === 0); })); - this.toUnbind.push(this.panelService.onDidPanelOpen(panel => this.refreshReplElements(true))); + this._register(this.panelService.onDidPanelOpen(panel => this.refreshReplElements(true))); } private refreshReplElements(noDelay: boolean): void { @@ -144,10 +149,6 @@ export class Repl extends Panel implements IPrivateReplService { controller }, replTreeOptions); - if (!Repl.HISTORY) { - Repl.HISTORY = new ReplHistory(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))); - } - return this.tree.setInput(this.debugService.getModel()); } @@ -155,7 +156,7 @@ export class Repl extends Panel implements IPrivateReplService { if (!visible) { dispose(this.model); } else { - this.model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:input`), true); + this.model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:replinput`), true); this.replInput.setModel(this.model); } @@ -165,63 +166,77 @@ export class Repl extends Panel implements IPrivateReplService { private createReplInput(container: HTMLElement): void { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); - const scopedContextKeyService = this.contextKeyService.createScoped(this.replInputContainer); - this.toUnbind.push(scopedContextKeyService); + const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: this.replInputContainer, historyNavigator: this }); + this.historyNavigationEnablement = historyNavigationEnablement; + this._register(scopedContextKeyService); CONTEXT_IN_DEBUG_REPL.bindTo(scopedContextKeyService).set(true); - const onFirstReplLine = CONTEXT_ON_FIRST_DEBUG_REPL_LINE.bindTo(scopedContextKeyService); - onFirstReplLine.set(true); - const onLastReplLine = CONTEXT_ON_LAST_DEBUG_REPL_LINE.bindTo(scopedContextKeyService); - onLastReplLine.set(true); const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, scopedContextKeyService], [IPrivateReplService, this])); - this.replInput = scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, SimpleDebugEditor.getEditorOptions(), SimpleDebugEditor.getCodeEditorWidgetOptions()); + this.replInput = scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, getSimpleEditorOptions(), getSimpleCodeEditorWidgetOptions()); - modes.SuggestRegistry.register({ scheme: DEBUG_SCHEME, hasAccessToAllModels: true }, { + modes.SuggestRegistry.register({ scheme: DEBUG_SCHEME, pattern: '**/replinput', hasAccessToAllModels: true }, { triggerCharacters: ['.'], provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext, token: CancellationToken): Thenable => { - const word = this.replInput.getModel().getWordAtPosition(position); - const overwriteBefore = word ? word.word.length : 0; - const text = this.replInput.getModel().getLineContent(position.lineNumber); - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; + // Disable history navigation because up and down are used to navigate through the suggest widget + this.historyNavigationEnablement.set(false); + const focusedSession = this.debugService.getViewModel().focusedSession; - const completions = focusedSession ? focusedSession.completions(frameId, text, position, overwriteBefore) : TPromise.as([]); - return wireCancellationToken(token, completions.then(suggestions => ({ - suggestions - }))); + if (focusedSession && focusedSession.capabilities.supportsCompletionsRequest) { + + const word = this.replInput.getModel().getWordAtPosition(position); + const overwriteBefore = word ? word.word.length : 0; + const text = this.replInput.getModel().getLineContent(position.lineNumber); + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; + + return focusedSession.completions(frameId, text, position, overwriteBefore).then(suggestions => { + return { suggestions }; + }, err => { + return { suggestions: [] }; + }); + } + return TPromise.as({ suggestions: [] }); } }); - this.toUnbind.push(this.replInput.onDidScrollChange(e => { + this._register(this.replInput.onDidScrollChange(e => { if (!e.scrollHeightChanged) { return; } this.replInputHeight = Math.max(Repl.REPL_INPUT_INITIAL_HEIGHT, Math.min(Repl.REPL_INPUT_MAX_HEIGHT, e.scrollHeight, this.dimension.height)); this.layout(this.dimension); })); - this.toUnbind.push(this.replInput.onDidChangeCursorPosition(e => { - onFirstReplLine.set(e.position.lineNumber === 1); - onLastReplLine.set(e.position.lineNumber === this.replInput.getModel().getLineCount()); + this._register(this.replInput.onDidChangeModelContent(() => { + this.historyNavigationEnablement.set(this.replInput.getModel().getValue() === ''); })); - this.toUnbind.push(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.FOCUS, () => dom.addClass(this.replInputContainer, 'synthetic-focus'))); - this.toUnbind.push(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => dom.removeClass(this.replInputContainer, 'synthetic-focus'))); + this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.FOCUS, () => dom.addClass(this.replInputContainer, 'synthetic-focus'))); + this._register(dom.addStandardDisposableListener(this.replInputContainer, dom.EventType.BLUR, () => dom.removeClass(this.replInputContainer, 'synthetic-focus'))); } - public navigateHistory(previous: boolean): void { - const historyInput = previous ? Repl.HISTORY.previous() : Repl.HISTORY.next(); + private navigateHistory(previous: boolean): void { + const historyInput = previous ? this.history.previous() : this.history.next(); if (historyInput) { - Repl.HISTORY.remember(this.replInput.getValue(), previous); this.replInput.setValue(historyInput); + aria.status(historyInput); // always leave cursor at the end. this.replInput.setPosition({ lineNumber: 1, column: historyInput.length + 1 }); + this.historyNavigationEnablement.set(true); } } + public showPreviousValue(): void { + this.navigateHistory(true); + } + + public showNextValue(): void { + this.navigateHistory(false); + } + public acceptReplInput(): void { this.debugService.addReplExpression(this.replInput.getValue()); - Repl.HISTORY.evaluated(this.replInput.getValue()); + this.history.add(this.replInput.getValue()); this.replInput.setValue(''); // Trigger a layout to shrink a potential multi line input this.replInputHeight = Repl.REPL_INPUT_INITIAL_HEIGHT; @@ -277,16 +292,14 @@ export class Repl extends Panel implements IPrivateReplService { this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL) ]; - this.actions.forEach(a => { - this.toUnbind.push(a); - }); + this.actions.forEach(a => this._register(a)); } return this.actions; } public shutdown(): void { - const replHistory = Repl.HISTORY.save(); + const replHistory = this.history.getHistory(); if (replHistory.length) { this.storageService.store(HISTORY_STORAGE_KEY, JSON.stringify(replHistory), StorageScope.WORKSPACE); } else { @@ -300,54 +313,6 @@ export class Repl extends Panel implements IPrivateReplService { } } -class ReplHistoryPreviousAction extends EditorAction { - - constructor() { - super({ - id: 'repl.action.historyPrevious', - label: nls.localize('actions.repl.historyPrevious', "History Previous"), - alias: 'History Previous', - precondition: CONTEXT_IN_DEBUG_REPL, - kbOpts: { - kbExpr: CONTEXT_ON_FIRST_DEBUG_REPL_LINE, - primary: KeyCode.UpArrow, - weight: 50 - }, - menuOpts: { - group: 'debug' - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void | TPromise { - accessor.get(IPrivateReplService).navigateHistory(true); - } -} - -class ReplHistoryNextAction extends EditorAction { - - constructor() { - super({ - id: 'repl.action.historyNext', - label: nls.localize('actions.repl.historyNext', "History Next"), - alias: 'History Next', - precondition: CONTEXT_IN_DEBUG_REPL, - kbOpts: { - kbExpr: CONTEXT_ON_LAST_DEBUG_REPL_LINE, - primary: KeyCode.DownArrow, - weight: 50 - }, - menuOpts: { - group: 'debug' - } - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void | TPromise { - accessor.get(IPrivateReplService).navigateHistory(false); - } -} - class AcceptReplInputAction extends EditorAction { constructor() { @@ -358,7 +323,8 @@ class AcceptReplInputAction extends EditorAction { precondition: CONTEXT_IN_DEBUG_REPL, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: KeyCode.Enter + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib } }); } @@ -385,8 +351,6 @@ export class ReplCopyAllAction extends EditorAction { } } -registerEditorAction(ReplHistoryPreviousAction); -registerEditorAction(ReplHistoryNextAction); registerEditorAction(AcceptReplInputAction); registerEditorAction(ReplCopyAllAction); diff --git a/src/vs/workbench/parts/debug/electron-browser/replViewer.ts b/src/vs/workbench/parts/debug/electron-browser/replViewer.ts index 0ff3980542b..008bb89550e 100644 --- a/src/vs/workbench/parts/debug/electron-browser/replViewer.ts +++ b/src/vs/workbench/parts/debug/electron-browser/replViewer.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; import * as lifecycle from 'vs/base/common/lifecycle'; -import * as errors from 'vs/base/common/errors'; import { isFullWidthCharacter, removeAnsiEscapeCodes, endsWith } from 'vs/base/common/strings'; import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import * as dom from 'vs/base/browser/dom'; @@ -24,6 +23,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LinkDetector } from 'vs/workbench/parts/debug/browser/linkDetector'; import { handleANSIOutput } from 'vs/workbench/parts/debug/browser/debugANSIHandling'; +import { ILabelService } from 'vs/platform/label/common/label'; const $ = dom.$; @@ -95,7 +95,8 @@ export class ReplExpressionsRenderer implements IRenderer { constructor( @IEditorService private editorService: IEditorService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @ILabelService private labelService: ILabelService ) { this.linkDetector = this.instantiationService.createInstance(LinkDetector); } @@ -201,7 +202,7 @@ export class ReplExpressionsRenderer implements IRenderer { startColumn: source.column, endLineNumber: source.lineNumber, endColumn: source.column - }).done(undefined, errors.onUnexpectedError); + }); } })); @@ -256,9 +257,9 @@ export class ReplExpressionsRenderer implements IRenderer { let result = handleANSIOutput(element.value, this.linkDetector); templateData.value.appendChild(result); - dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : 'info'); + dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info'); templateData.source.textContent = element.sourceData ? `${element.sourceData.source.name}:${element.sourceData.lineNumber}` : ''; - templateData.source.title = element.sourceData ? element.sourceData.source.uri.toString() : ''; + templateData.source.title = element.sourceData ? this.labelService.getUriLabel(element.sourceData.source.uri) : ''; templateData.getReplElementSource = () => element.sourceData; } diff --git a/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts b/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts deleted file mode 100644 index 720b75f1cad..00000000000 --- a/src/vs/workbench/parts/debug/electron-browser/simpleDebugEditor.ts +++ /dev/null @@ -1,57 +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 { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; - -// Allowed Editor Contributions: -import { MenuPreventer } from 'vs/workbench/parts/codeEditor/electron-browser/menuPreventer'; -import { SelectionClipboard } from 'vs/workbench/parts/codeEditor/electron-browser/selectionClipboard'; -import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; -import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { TabCompletionController } from 'vs/workbench/parts/snippets/electron-browser/tabCompletion'; - -export class SimpleDebugEditor { - - public static getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { - return { - isSimpleWidget: true, - contributions: [ - MenuPreventer, - SelectionClipboard, - ContextMenuController, - SuggestController, - SnippetController2, - TabCompletionController, - ] - }; - } - - public static getEditorOptions(): IEditorOptions { - return { - wordWrap: 'on', - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - selectOnLineNumbers: false, - hideCursorInOverviewRuler: true, - selectionHighlight: false, - scrollbar: { - horizontal: 'hidden' - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - acceptSuggestionOnEnter: 'smart', - minimap: { - enabled: false - } - }; - } -} diff --git a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts index e9abd1de5f1..91fc2c53456 100644 --- a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts +++ b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts @@ -11,18 +11,21 @@ import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; import { hasChildprocesses, prepareCommand } from 'vs/workbench/parts/debug/node/terminals'; -export class AbstractTerminalLauncher implements ITerminalLauncher { +export class TerminalLauncher implements ITerminalLauncher { private integratedTerminalInstance: ITerminalInstance; private terminalDisposedListener: IDisposable; - constructor(private terminalService: ITerminalService) { + constructor( + @ITerminalService private terminalService: ITerminalService, + @IExternalTerminalService private nativeTerminalService: IExternalTerminalService + ) { } - async runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'external') { - return this.runInExternalTerminal(args, config); + return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); } if (!this.terminalDisposedListener) { @@ -35,46 +38,19 @@ export class AbstractTerminalLauncher implements ITerminalLauncher { } let t = this.integratedTerminalInstance; - if ((t && await this.isBusy(t.processId)) || !t) { + if ((t && hasChildprocesses(t.processId)) || !t) { t = this.terminalService.createTerminal({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); this.integratedTerminalInstance = t; } this.terminalService.setActiveInstance(t); this.terminalService.showPanel(true); - const command = await this.prepareCommand(args, config); - return new TPromise((resolve, error) => { setTimeout(_ => { + const command = prepareCommand(args, config); t.sendText(command, true); resolve(void 0); }, 500); }); } - - protected runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return void 0; - } - - protected isBusy(processId: number): TPromise { - return TPromise.as(hasChildprocesses(processId)); - } - - protected prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return TPromise.as(prepareCommand(args, config)); - } -} - -export class TerminalLauncher extends AbstractTerminalLauncher { - - constructor( - @ITerminalService terminalService: ITerminalService, - @IExternalTerminalService private nativeTerminalService: IExternalTerminalService - ) { - super(terminalService); - } - - runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); - } } diff --git a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts index 20121402450..076d4cb8f90 100644 --- a/src/vs/workbench/parts/debug/electron-browser/variablesView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/variablesView.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { RunOnceScheduler, sequence } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import * as errors from 'vs/base/common/errors'; import { IActionProvider, ITree, IDataSource, IRenderer, IAccessibilityProvider } from 'vs/base/parts/tree/browser/tree'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -77,7 +76,7 @@ export class VariablesView extends TreeViewsViewletPanel { } return undefined; }); - }).done(null, errors.onUnexpectedError); + }); }, 400); } @@ -124,7 +123,7 @@ export class VariablesView extends TreeViewsViewletPanel { this.disposables.push(this.debugService.getViewModel().onDidSelectExpression(expression => { if (expression instanceof Variable) { - this.tree.refresh(expression, false).done(null, errors.onUnexpectedError); + this.tree.refresh(expression, false); } })); } @@ -318,7 +317,7 @@ class VariablesController extends BaseDebugController { protected onLeftClick(tree: ITree, element: any, event: IMouseEvent): boolean { // double click on primitive value: open input box to be able to set the value const session = this.debugService.getViewModel().focusedSession; - if (element instanceof Variable && event.detail === 2 && session && session.raw.capabilities.supportsSetVariable) { + if (element instanceof Variable && event.detail === 2 && session && session.capabilities.supportsSetVariable) { const expression = element; this.debugService.getViewModel().setSelectedExpression(expression); return true; diff --git a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts index c1b3079a4c2..36de5bfdf70 100644 --- a/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/watchExpressionsView.ts @@ -8,7 +8,6 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as errors from 'vs/base/common/errors'; import { IActionProvider, ITree, IDataSource, IRenderer, IAccessibilityProvider, IDragAndDropData, IDragOverReaction, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -50,12 +49,12 @@ export class WatchExpressionsView extends TreeViewsViewletPanel { @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('expressionsSection', "Expressions Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService); this.settings = options.viewletSettings; this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; - this.tree.refresh().done(undefined, errors.onUnexpectedError); + this.tree.refresh(); }, 50); } @@ -90,9 +89,9 @@ export class WatchExpressionsView extends TreeViewsViewletPanel { return; } - this.tree.refresh().done(() => { + this.tree.refresh().then(() => { return we instanceof Expression ? this.tree.reveal(we) : TPromise.as(true); - }, errors.onUnexpectedError); + }); })); this.disposables.push(this.debugService.getViewModel().onDidFocusStackFrame(() => { if (!this.isExpanded() || !this.isVisible()) { @@ -107,7 +106,7 @@ export class WatchExpressionsView extends TreeViewsViewletPanel { this.disposables.push(this.debugService.getViewModel().onDidSelectExpression(expression => { if (expression instanceof Expression) { - this.tree.refresh(expression, false).done(null, errors.onUnexpectedError); + this.tree.refresh(expression, false); } })); } @@ -177,7 +176,7 @@ class WatchExpressionsActionProvider implements IActionProvider { if (element instanceof Variable) { const variable = element; if (!variable.hasChildren) { - actions.push(new CopyValueAction(CopyValueAction.ID, CopyValueAction.LABEL, variable.value, this.debugService)); + actions.push(new CopyValueAction(CopyValueAction.ID, CopyValueAction.LABEL, variable, this.debugService)); } actions.push(new Separator()); } @@ -300,16 +299,17 @@ class WatchExpressionsRenderer implements IRenderer { } data.name.textContent = watchExpression.name; - if (watchExpression.value) { + renderExpressionValue(watchExpression, data.value, { + showChanged: true, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + preserveWhitespace: false, + showHover: true, + colorize: true + }); + data.name.title = watchExpression.type ? watchExpression.type : watchExpression.value; + + if (typeof watchExpression.value === 'string') { data.name.textContent += ':'; - renderExpressionValue(watchExpression, data.value, { - showChanged: true, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - preserveWhitespace: false, - showHover: true, - colorize: true - }); - data.name.title = watchExpression.type ? watchExpression.type : watchExpression.value; } } diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index befbb0fd4e8..12bb2e8e956 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -12,16 +12,12 @@ import * as paths from 'vs/base/common/paths'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; -import * as stdfork from 'vs/base/node/stdFork'; import { Emitter, Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; -import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IConfig } from 'vs/workbench/parts/debug/common/debug'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution } from 'vs/workbench/parts/debug/common/debug'; /** * Abstract implementation of the low level API for a debug adapter. @@ -39,7 +35,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { constructor() { this.sequence = 1; - this.pendingRequests = new Map void>(); + this.pendingRequests = new Map(); this._onError = new Emitter(); this._onExit = new Emitter(); @@ -83,7 +79,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { } } - public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { + public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): void { const request: any = { command: command @@ -94,6 +90,25 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.internalSend('request', request); + if (typeof timeout === 'number') { + const timer = setTimeout(() => { + clearTimeout(timer); + const clb = this.pendingRequests.get(request.seq); + if (clb) { + this.pendingRequests.delete(request.seq); + const err: DebugProtocol.Response = { + type: 'response', + seq: 0, + request_seq: request.seq, + success: false, + command, + message: `timeout after ${timeout} ms` + }; + clb(err); + } + }, timeout); + } + if (clb) { // store callback for this request this.pendingRequests.set(request.seq, clb); @@ -130,6 +145,24 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.sendMessage(message); } + + protected cancelPending() { + const pending = this.pendingRequests; + this.pendingRequests = new Map(); + setTimeout(_ => { + pending.forEach((callback, request_seq) => { + const err: DebugProtocol.Response = { + type: 'response', + seq: 0, + request_seq, + success: false, + command: 'canceled', + message: 'canceled' + }; + callback(err); + }); + }, 1000); + } } /** @@ -149,7 +182,7 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { super(); } - public connect(readable: stream.Readable, writable: stream.Writable): void { + protected connect(readable: stream.Readable, writable: stream.Writable): void { this.outputStream = writable; this.rawData = Buffer.allocUnsafe(0); @@ -157,16 +190,16 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { readable.on('data', (data: Buffer) => this.handleData(data)); - // readable.on('close', () => { - // this._emitEvent(new Event('close')); - // }); - // readable.on('error', (error) => { - // this._emitEvent(new Event('error', 'readable error: ' + (error && error.message))); - // }); + readable.on('close', () => { + this._onError.fire(new Error('readable.close event')); + }); + readable.on('error', (error) => { + this._onError.fire(error); + }); - // writable.on('error', (error) => { - // this._emitEvent(new Event('error', 'writable error: ' + (error && error.message))); - // }); + writable.on('error', (error) => { + this._onError.fire(error); + }); } public sendMessage(message: DebugProtocol.ProtocolMessage): void { @@ -241,11 +274,15 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } stopSession(): TPromise { - if (this.socket !== null) { + + // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. + this.cancelPending(); + + if (this.socket) { this.socket.end(); this.socket = undefined; } - return void 0; + return TPromise.as(undefined); } } @@ -286,15 +323,17 @@ export class DebugAdapter extends StreamDebugAdapter { "Cannot determine executable for debug adapter '{0}'.", this.debugType))); } - if (this.adapterExecutable.command === 'node' && this.outputService) { + if (this.adapterExecutable.command === 'node') { if (Array.isArray(this.adapterExecutable.args) && this.adapterExecutable.args.length > 0) { - stdfork.fork(this.adapterExecutable.args[0], this.adapterExecutable.args.slice(1), {}, (err, child) => { - if (err) { - e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", this.adapterExecutable.args[0]))); - } - this.serverProcess = child; - c(null); + const child = cp.fork(this.adapterExecutable.args[0], this.adapterExecutable.args.slice(1), { + execArgv: ['-e', 'delete process.env.ELECTRON_RUN_AS_NODE;require(process.argv[1])'], + silent: true }); + if (!child.pid) { + e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", this.adapterExecutable.args[0]))); + } + this.serverProcess = child; + c(null); } else { e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."))); } @@ -324,6 +363,13 @@ export class DebugAdapter extends StreamDebugAdapter { stopSession(): TPromise { + // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. + this.cancelPending(); + + if (!this.serverProcess) { + return TPromise.as(null); + } + // when killing a process in windows its child // processes are *not* killed but become root // processes. Therefore we use TASKKILL.EXE @@ -399,7 +445,6 @@ export class DebugAdapter extends StreamDebugAdapter { if (debuggers && debuggers.length > 0) { debuggers.filter(dbg => strings.equalsIgnoreCase(dbg.type, debugType)).forEach(dbg => { // extract relevant attributes and make then absolute where needed - // TODO@extensionLocation const extractedDbg = DebugAdapter.extract(dbg, ed.extensionLocation.fsPath); // merge @@ -440,26 +485,4 @@ export class DebugAdapter extends StreamDebugAdapter { }; } } - - static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService, commandValueMapping?: IStringDictionary): IConfig { - - const result = objects.deepClone(config) as IConfig; - - // hoist platform specific attributes to top level - if (platform.isWindows && result.windows) { - Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); - } else if (platform.isMacintosh && result.osx) { - Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); - } else if (platform.isLinux && result.linux) { - Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); - } - - // delete all platform specific sections - delete result.windows; - delete result.osx; - delete result.linux; - - // substitute all variables in string values - return resolverService.resolveAny(workspaceFolder, result, commandValueMapping); - } } diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index b16771d21d2..169c8e1fab5 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -8,10 +8,10 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings, IDebugger } from 'vs/workbench/parts/debug/common/debug'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -19,11 +19,12 @@ import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { DebugAdapter, SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import uri from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { memoize } from 'vs/base/common/decorators'; +import { TaskDefinitionRegistry } from 'vs/workbench/parts/tasks/common/taskDefinitionRegistry'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; -export class Debugger { +export class Debugger implements IDebugger { private mergedExtensionDescriptions: IExtensionDescription[]; @@ -72,23 +73,13 @@ export class Debugger { } public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { - - // first resolve command variables (which might have a UI) - return this.configurationResolverService.executeCommandVariables(config, this.variables).then(commandValueMapping => { - - if (!commandValueMapping) { // cancelled by user - return null; - } - - // now substitute all other variables - return (this.inEH() ? this.configurationManager.substituteVariables(this.type, folder, config) : TPromise.as(config)).then(config => { - try { - return TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService, commandValueMapping)); - } catch (e) { - return TPromise.wrapError(e); - } + if (this.inEH()) { + return this.configurationManager.substituteVariables(this.type, folder, config).then(config => { + return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); }); - }); + } else { + return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); + } } public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise { @@ -98,7 +89,7 @@ export class Debugger { private inEH(): boolean { const debugConfigs = this.configurationService.getValue('debug'); - return debugConfigs.extensionHostDebugAdapter; + return debugConfigs.extensionHostDebugAdapter || this.extensionDescription.extensionLocation.scheme !== 'file'; } public get label(): string { @@ -182,7 +173,7 @@ export class Debugger { return telemetryInfo; }).then(data => { const client = new TelemetryClient( - uri.parse(require.toUrl('bootstrap')).fsPath, + getPathFromAmdModule(require, 'bootstrap-fork'), { serverName: 'Debug Telemetry', timeout: 1000 * 60 * 5, @@ -207,6 +198,7 @@ export class Debugger { return null; } // fill in the default configuration attributes shared by all adapters. + const taskSchema = TaskDefinitionRegistry.getJsonSchema(); return Object.keys(this.debuggerContribution.configurationAttributes).map(request => { const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; const defaultRequired = ['name', 'type', 'request']; @@ -239,12 +231,16 @@ export class Debugger { default: 4711 }; properties['preLaunchTask'] = { - type: ['string', 'null'], + anyOf: [taskSchema, { + type: ['string', 'null'], + }], default: '', description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.") }; properties['postDebugTask'] = { - type: ['string', 'null'], + anyOf: [taskSchema, { + type: ['string', 'null'], + }], default: '', description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") }; diff --git a/src/vs/workbench/parts/debug/node/telemetryApp.ts b/src/vs/workbench/parts/debug/node/telemetryApp.ts index db5e7ec6f62..490dc714fcc 100644 --- a/src/vs/workbench/parts/debug/node/telemetryApp.ts +++ b/src/vs/workbench/parts/debug/node/telemetryApp.ts @@ -5,7 +5,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; const appender = new AppInsightsAppender(process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.dispose()); diff --git a/src/vs/workbench/parts/debug/node/terminals.ts b/src/vs/workbench/parts/debug/node/terminals.ts index a46ea642248..a50d767ded0 100644 --- a/src/vs/workbench/parts/debug/node/terminals.ts +++ b/src/vs/workbench/parts/debug/node/terminals.ts @@ -11,8 +11,8 @@ import * as env from 'vs/base/common/platform'; import * as pfs from 'vs/base/node/pfs'; import { assign } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; -import uri from 'vs/base/common/uri'; import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); @@ -55,7 +55,7 @@ export function getDefaultTerminalLinuxReady(): TPromise { } c('xterm'); - }, () => { }); + }); } return _DEFAULT_TERMINAL_LINUX_READY; } @@ -132,7 +132,7 @@ class MacTerminalService extends TerminalLauncher { // and then launches the program inside that window. const script = terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper'; - const scriptpath = uri.parse(require.toUrl(`vs/workbench/parts/execution/electron-browser/${script}.scpt`)).fsPath; + const scriptpath = getPathFromAmdModule(require, `vs/workbench/parts/execution/electron-browser/${script}.scpt`); const osaArgs = [ scriptpath, @@ -312,7 +312,7 @@ export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments // try to determine the shell type shell = shell.trim().toLowerCase(); - if (shell.indexOf('powershell') >= 0) { + if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0) { shellType = ShellType.powershell; } else if (shell.indexOf('cmd.exe') >= 0) { shellType = ShellType.cmd; @@ -415,4 +415,4 @@ export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments } return command; -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/parts/debug/test/browser/baseDebugView.test.ts index d52a69598b8..837cb46a9b6 100644 --- a/src/vs/workbench/parts/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/parts/debug/test/browser/baseDebugView.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { replaceWhitespace, renderExpressionValue, renderVariable } from 'vs/workbench/parts/debug/browser/baseDebugView'; import * as dom from 'vs/base/browser/dom'; -import { Expression, Variable, Session, Scope, StackFrame, Thread } from 'vs/workbench/parts/debug/common/debugModel'; +import { Expression, Variable, Scope, StackFrame, Thread } from 'vs/workbench/parts/debug/common/debugModel'; import { MockSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; const $ = dom.$; @@ -52,8 +52,7 @@ suite('Debug - Base Debug View', () => { }); test('render variable', () => { - const rawSession = new MockSession(); - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new MockSession(); const thread = new Thread(session, 'mockthread', 1); const stackFrame = new StackFrame(thread, 1, null, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined, endColumn: undefined }, 0); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); diff --git a/src/vs/workbench/parts/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/parts/debug/test/browser/debugANSIHandling.test.ts index 9c7fb85fc49..c8e8ed02750 100644 --- a/src/vs/workbench/parts/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/parts/debug/test/browser/debugANSIHandling.test.ts @@ -40,7 +40,7 @@ suite('Debug - ANSI Handling', () => { assert(dom.hasClass(child, 'class1')); assert(dom.hasClass(child, 'class2')); } else { - assert.fail(); + assert.fail('Unexpected assertion error'); } child = root.lastChild; @@ -49,7 +49,7 @@ suite('Debug - ANSI Handling', () => { assert(dom.hasClass(child, 'class2')); assert(dom.hasClass(child, 'class3')); } else { - assert.fail(); + assert.fail('Unexpected assertion error'); } }); @@ -66,7 +66,7 @@ suite('Debug - ANSI Handling', () => { if (child instanceof HTMLSpanElement) { return child; } else { - assert.fail(); + assert.fail('Unexpected assertion error'); return null; } } @@ -170,7 +170,7 @@ suite('Debug - ANSI Handling', () => { if (child instanceof HTMLSpanElement) { assertions[i](child); } else { - assert.fail(); + assert.fail('Unexpected assertion error'); } } } diff --git a/src/vs/workbench/parts/debug/test/common/debugSource.test.ts b/src/vs/workbench/parts/debug/test/common/debugSource.test.ts index fd2656f96a6..807a2c0ba03 100644 --- a/src/vs/workbench/parts/debug/test/common/debugSource.test.ts +++ b/src/vs/workbench/parts/debug/test/common/debugSource.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { normalize } from 'vs/base/common/paths'; diff --git a/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts b/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts index 733ae781c37..a466b211fe4 100644 --- a/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts +++ b/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel'; -import { StackFrame, Expression, Thread, Session } from 'vs/workbench/parts/debug/common/debugModel'; +import { StackFrame, Expression, Thread } from 'vs/workbench/parts/debug/common/debugModel'; import { MockSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; @@ -23,8 +23,7 @@ suite('Debug - View Model', () => { test('focused stack frame', () => { assert.equal(model.focusedStackFrame, null); assert.equal(model.focusedThread, null); - const mockSession = new MockSession(); - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, mockSession); + const session = new MockSession(); const thread = new Thread(session, 'myThread', 1); const frame = new StackFrame(thread, 1, null, 'app.js', 'normal', { startColumn: 1, startLineNumber: 1, endColumn: undefined, endLineNumber: undefined }, 0); model.setFocus(frame, thread, session, false); diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index aaa7993b320..bd2be3b46d2 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -3,28 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import uri from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; +import { URI as uri } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ILaunch, IDebugService, State, DebugEvent, ISession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawSession, IBreakpoint } from 'vs/workbench/parts/debug/common/debug'; +import { Position } from 'vs/editor/common/core/position'; +import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, ActualBreakpoints, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent } from 'vs/workbench/parts/debug/common/debug'; +import { Source } from 'vs/workbench/parts/debug/common/debugSource'; +import { ISuggestion } from 'vs/editor/common/modes'; export class MockDebugService implements IDebugService { + public _serviceBrand: any; + getSession(sessionId: string): IDebugSession { + return undefined; + } + public get state(): State { return null; } - public get onDidCustomEvent(): Event { + public get onWillNewSession(): Event { return null; } - public get onDidNewSession(): Event { + public get onDidNewSession(): Event { return null; } - public get onDidEndSession(): Event { + public get onDidEndSession(): Event { return null; } @@ -39,6 +47,10 @@ export class MockDebugService implements IDebugService { public focusStackFrame(focusedStackFrame: IStackFrame): void { } + sendAllBreakpoints(session?: IDebugSession): TPromise { + return TPromise.as(null); + } + public addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): TPromise { return TPromise.as(null); } @@ -108,19 +120,161 @@ export class MockDebugService implements IDebugService { public logToRepl(value: string): void { } public sourceIsNotAvailable(uri: uri): void { } + + public tryToAutoFocusStackFrame(thread: IThread): TPromise { + return TPromise.as(null); + } } -export class MockSession implements IRawSession { +export class MockSession implements IDebugSession { + + configuration: IConfig = { type: 'mock', request: 'launch' }; + unresolvedConfiguration: IConfig = { type: 'mock', request: 'launch' }; + state = State.Stopped; + root: IWorkspaceFolder; + capabilities: DebugProtocol.Capabilities = {}; + + getId(): string { + return 'mock'; + } + + getName(includeRoot: boolean): string { + return 'mockname'; + } + + getSourceForUri(modelUri: uri): Source { + return null; + } + + getThread(threadId: number): IThread { + return null; + } + + get onDidCustomEvent(): Event { + return null; + } + + get onDidLoadedSource(): Event { + return null; + } + + get onDidChangeState(): Event { + return null; + } + + get onDidEndAdapter(): Event { + return null; + } + + getAllThreads(): ReadonlyArray { + return []; + } + + getSource(raw: DebugProtocol.Source): Source { + return undefined; + } + + getLoadedSources(): TPromise { + return TPromise.as([]); + } + + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { + return TPromise.as([]); + } + + clearThreads(removeThreads: boolean, reference?: number): void { } + + rawUpdate(data: IRawModelUpdate): void { } + + initialize(dbgr: IDebugger): TPromise { + throw new Error('Method not implemented.'); + } + launchOrAttach(config: IConfig): TPromise { + throw new Error('Method not implemented.'); + } + restart(): TPromise { + throw new Error('Method not implemented.'); + } + sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): TPromise { + throw new Error('Method not implemented.'); + } + sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): TPromise { + throw new Error('Method not implemented.'); + } + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise { + throw new Error('Method not implemented.'); + } + customRequest(request: string, args: any): TPromise { + throw new Error('Method not implemented.'); + } + stackTrace(threadId: number, startFrame: number, levels: number): TPromise { + throw new Error('Method not implemented.'); + } + exceptionInfo(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + scopes(frameId: number): TPromise { + throw new Error('Method not implemented.'); + } + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise { + throw new Error('Method not implemented.'); + } + evaluate(expression: string, frameId: number, context?: string): TPromise { + throw new Error('Method not implemented.'); + } + restartFrame(frameId: number, threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + next(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepIn(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepOut(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepBack(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + continue(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + reverseContinue(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + pause(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + terminateThreads(threadIds: number[]): TPromise { + throw new Error('Method not implemented.'); + } + setVariable(variablesReference: number, name: string, value: string): TPromise { + throw new Error('Method not implemented.'); + } + loadSource(resource: uri): TPromise { + throw new Error('Method not implemented.'); + } + + terminate(restart = false): TPromise { + throw new Error('Method not implemented.'); + } + disconnect(restart = false): TPromise { + throw new Error('Method not implemented.'); + } + + dispose(): void { } +} + +export class MockRawSession { + + capabilities: DebugProtocol.Capabilities; + disconnected: boolean; + sessionLengthInSeconds: number; public readyForBreakpoints = true; public emittedStopped = true; - public getId() { - return 'mockrawsession'; - } - - public root: IWorkspaceFolder; - public getLengthInSeconds(): number { return 100; } @@ -147,7 +301,7 @@ export class MockSession implements IRawSession { return TPromise.as(null); } - public attach(args: DebugProtocol.AttachRequestArguments): TPromise { + public launchOrAttach(args: IConfig): TPromise { return TPromise.as(null); } @@ -163,30 +317,15 @@ export class MockSession implements IRawSession { return TPromise.as(null); } - public get capabilities(): DebugProtocol.Capabilities { - return {}; - } - - public get onDidEvent(): Event { - return null; - } - - public get onDidInitialize(): Event { - const emitter = new Emitter(); - return emitter.event; - } - - public get onDidExitAdapter(): Event<{ sessionId: string }> { - const emitter = new Emitter<{ sessionId: string }>(); - return emitter.event; - } - - public custom(request: string, args: any): TPromise { return TPromise.as(null); } - public disconnect(restart?: boolean, force?: boolean): TPromise { + public terminate(restart = false): TPromise { + return TPromise.as(null); + } + + public disconnect(restart?: boolean): TPromise { return TPromise.as(null); } @@ -218,7 +357,7 @@ export class MockSession implements IRawSession { return TPromise.as(null); } - public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise { + public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise { return TPromise.as(null); } @@ -242,6 +381,10 @@ export class MockSession implements IRawSession { return TPromise.as(null); } + public loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise { + return TPromise.as(null); + } + public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/debug/test/common/replHistory.test.ts b/src/vs/workbench/parts/debug/test/common/replHistory.test.ts deleted file mode 100644 index 5f8639491dd..00000000000 --- a/src/vs/workbench/parts/debug/test/common/replHistory.test.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 assert from 'assert'; -import { ReplHistory } from 'vs/workbench/parts/debug/common/replHistory'; - -suite('Debug - Repl History', () => { - let history: ReplHistory; - - setup(() => { - history = new ReplHistory(['one', 'two', 'three', 'four', 'five']); - }); - - teardown(() => { - history = null; - }); - - test('previous and next', () => { - assert.equal(history.previous(), 'five'); - assert.equal(history.previous(), 'four'); - assert.equal(history.previous(), 'three'); - assert.equal(history.previous(), 'two'); - assert.equal(history.previous(), 'one'); - assert.equal(history.previous(), null); - assert.equal(history.next(), 'two'); - assert.equal(history.next(), 'three'); - assert.equal(history.next(), 'four'); - assert.equal(history.next(), 'five'); - }); - - test('evaluated and remember', () => { - history.evaluated('six'); - assert.equal(history.previous(), 'six'); - assert.equal(history.previous(), 'five'); - assert.equal(history.next(), 'six'); - - history.remember('six++', true); - assert.equal(history.next(), 'six++'); - assert.equal(history.previous(), 'six'); - - history.evaluated('seven'); - assert.equal(history.previous(), 'seven'); - assert.equal(history.previous(), 'six'); - }); -}); diff --git a/src/vs/workbench/parts/debug/test/node/debugModel.test.ts b/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts similarity index 86% rename from src/vs/workbench/parts/debug/test/node/debugModel.test.ts rename to src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts index e480fb71cb6..8785367244f 100644 --- a/src/vs/workbench/parts/debug/test/node/debugModel.test.ts +++ b/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts @@ -4,20 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import severity from 'vs/base/common/severity'; -import { SimpleReplElement, Model, Session, Expression, RawObjectReplElement, StackFrame, Thread } from 'vs/workbench/parts/debug/common/debugModel'; +import { SimpleReplElement, Model, Expression, RawObjectReplElement, StackFrame, Thread } from 'vs/workbench/parts/debug/common/debugModel'; import * as sinon from 'sinon'; -import { MockSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; +import { MockRawSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; +import { DebugSession } from 'vs/workbench/parts/debug/electron-browser/debugSession'; suite('Debug - Model', () => { let model: Model; - let rawSession: MockSession; + let rawSession: MockRawSession; setup(() => { - model = new Model([], true, [], [], []); - rawSession = new MockSession(); + model = new Model([], true, [], [], [], { isDirty: (e: any) => false }); + rawSession = new MockRawSession(); }); teardown(() => { @@ -108,18 +109,18 @@ suite('Debug - Model', () => { test('threads simple', () => { const threadId = 1; const threadName = 'firstThread'; + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + model.addSession(session); - model.addSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); assert.equal(model.getSessions().length, 1); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: threadId, thread: { id: threadId, name: threadName } }); - const session = model.getSessions().filter(p => p.getId() === rawSession.getId()).pop(); assert.equal(session.getThread(threadId).name, threadName); @@ -140,9 +141,13 @@ suite('Debug - Model', () => { const stoppedReason = 'breakpoint'; // Add the threads - model.addSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + model.addSession(session); + + session['_raw'] = rawSession; + model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: threadId1, thread: { id: threadId1, @@ -151,7 +156,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: threadId2, thread: { id: threadId2, @@ -161,7 +166,7 @@ suite('Debug - Model', () => { // Stopped event with all threads stopped model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: threadId1, stoppedDetails: { reason: stoppedReason, @@ -169,7 +174,6 @@ suite('Debug - Model', () => { allThreadsStopped: true }, }); - const session = model.getSessions().filter(p => p.getId() === rawSession.getId()).pop(); const thread1 = session.getThread(threadId1); const thread2 = session.getThread(threadId2); @@ -230,10 +234,14 @@ suite('Debug - Model', () => { const runningThreadId = 2; const runningThreadName = 'runningThread'; const stoppedReason = 'breakpoint'; - model.addSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + model.addSession(session); + + session['_raw'] = rawSession; + // Add the threads model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: stoppedThreadId, thread: { id: stoppedThreadId, @@ -242,7 +250,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: runningThreadId, thread: { id: runningThreadId, @@ -252,7 +260,7 @@ suite('Debug - Model', () => { // Stopped event with only one thread stopped model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: session.getId(), threadId: stoppedThreadId, stoppedDetails: { reason: stoppedReason, @@ -260,7 +268,6 @@ suite('Debug - Model', () => { allThreadsStopped: false } }); - const session = model.getSessions().filter(p => p.getId() === rawSession.getId()).pop(); const stoppedThread = session.getThread(stoppedThreadId); const runningThread = session.getThread(runningThreadId); @@ -341,12 +348,15 @@ suite('Debug - Model', () => { test('repl expressions', () => { assert.equal(model.getReplElements().length, 0); - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + model.addSession(session); + + session['_raw'] = rawSession; const thread = new Thread(session, 'mockthread', 1); const stackFrame = new StackFrame(thread, 1, null, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - model.addReplExpression(session, stackFrame, 'myVariable').done(); - model.addReplExpression(session, stackFrame, 'myVariable').done(); - model.addReplExpression(session, stackFrame, 'myVariable').done(); + model.addReplExpression(session, stackFrame, 'myVariable').then(); + model.addReplExpression(session, stackFrame, 'myVariable').then(); + model.addReplExpression(session, stackFrame, 'myVariable').then(); assert.equal(model.getReplElements().length, 3); model.getReplElements().forEach(re => { @@ -360,7 +370,9 @@ suite('Debug - Model', () => { }); test('stack frame get specific source name', () => { - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + model.addSession(session); + let firstStackFrame: StackFrame; let secondStackFrame: StackFrame; const thread = new class extends Thread { diff --git a/src/vs/workbench/parts/debug/test/node/debugger.test.ts b/src/vs/workbench/parts/debug/test/node/debugger.test.ts index a0b9a5dabee..ba40f30db8e 100644 --- a/src/vs/workbench/parts/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/parts/debug/test/node/debugger.test.ts @@ -9,7 +9,7 @@ import * as platform from 'vs/base/common/platform'; import { IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { DebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; diff --git a/src/vs/workbench/parts/emmet/browser/actions/showEmmetCommands.ts b/src/vs/workbench/parts/emmet/browser/actions/showEmmetCommands.ts index ea18fc77506..c144579d4aa 100644 --- a/src/vs/workbench/parts/emmet/browser/actions/showEmmetCommands.ts +++ b/src/vs/workbench/parts/emmet/browser/actions/showEmmetCommands.ts @@ -12,6 +12,7 @@ import { registerEditorAction, EditorAction, ServicesAccessor } from 'vs/editor/ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { MenuId } from 'vs/platform/actions/common/actions'; const EMMET_COMMANDS_PREFIX = '>Emmet: '; @@ -23,6 +24,12 @@ class ShowEmmetCommandsAction extends EditorAction { label: nls.localize('showEmmetCommands', "Show Emmet Commands"), alias: 'Show Emmet Commands', precondition: EditorContextKeys.writable, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '5_insert', + title: nls.localize({ key: 'miShowEmmetCommands', comment: ['&& denotes a mnemonic'] }, "E&&mmet..."), + order: 4 + } }); } diff --git a/src/vs/workbench/parts/emmet/electron-browser/actions/expandAbbreviation.ts b/src/vs/workbench/parts/emmet/electron-browser/actions/expandAbbreviation.ts index 5a9512f8999..42968c4b846 100644 --- a/src/vs/workbench/parts/emmet/electron-browser/actions/expandAbbreviation.ts +++ b/src/vs/workbench/parts/emmet/electron-browser/actions/expandAbbreviation.ts @@ -10,6 +10,8 @@ import { registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuId } from 'vs/platform/actions/common/actions'; class ExpandAbbreviationAction extends EmmetEditorAction { @@ -26,7 +28,14 @@ class ExpandAbbreviationAction extends EmmetEditorAction { EditorContextKeys.editorTextFocus, EditorContextKeys.tabDoesNotMoveFocus, ContextKeyExpr.has('config.emmet.triggerExpansionOnTab') - ) + ), + weight: KeybindingWeight.EditorContrib + }, + menubarOpts: { + menuId: MenuId.MenubarEditMenu, + group: '5_insert', + title: nls.localize({ key: 'miEmmetExpandAbbreviation', comment: ['&& denotes a mnemonic'] }, "Emmet: E&&xpand Abbreviation"), + order: 3 } }); diff --git a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts index 3bd97be51bb..92e979c5480 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import * as paths from 'vs/base/common/paths'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { ITerminalService } from 'vs/workbench/parts/execution/common/execution'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -20,7 +20,7 @@ import { getDefaultTerminalWindows, getDefaultTerminalLinuxReady, DEFAULT_TERMIN import { WinTerminalService, MacTerminalService, LinuxTerminalService } from 'vs/workbench/parts/execution/electron-browser/terminalService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IFileService } from 'vs/platform/files/common/files'; import { IListService } from 'vs/platform/list/browser/listService'; import { getMultiSelectedResources } from 'vs/workbench/parts/files/browser/files'; @@ -40,37 +40,41 @@ if (env.isWindows) { getDefaultTerminalLinuxReady().then(defaultTerminalLinux => { let configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'externalTerminal', - 'order': 100, - 'title': nls.localize('terminalConfigurationTitle', "External Terminal"), - 'type': 'object', - 'properties': { + id: 'externalTerminal', + order: 100, + title: nls.localize('terminalConfigurationTitle', "External Terminal"), + type: 'object', + properties: { 'terminal.explorerKind': { - 'type': 'string', - 'enum': [ + type: 'string', + enum: [ 'integrated', 'external' ], - 'description': nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), - 'default': 'integrated' + enumDescriptions: [ + nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."), + nls.localize('terminal.explorerKind.external', "Use the configured external terminal.") + ], + description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."), + default: 'integrated' }, 'terminal.external.windowsExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), - 'default': getDefaultTerminalWindows(), - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."), + default: getDefaultTerminalWindows(), + scope: ConfigurationScope.APPLICATION }, 'terminal.external.osxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on OS X."), - 'default': DEFAULT_TERMINAL_OSX, - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."), + default: DEFAULT_TERMINAL_OSX, + scope: ConfigurationScope.APPLICATION }, 'terminal.external.linuxExec': { - 'type': 'string', - 'description': nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), - 'default': defaultTerminalLinux, - 'scope': ConfigurationScope.APPLICATION + type: 'string', + description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."), + default: defaultTerminalLinux, + scope: ConfigurationScope.APPLICATION } } }); @@ -109,7 +113,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: OPEN_NATIVE_CONSOLE_COMMAND_ID, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C, when: KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, handler: (accessor) => { const historyService = accessor.get(IHistoryService); const terminalService = accessor.get(ITerminalService); @@ -118,7 +122,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ terminalService.openTerminal(root.fsPath); } else { // Opens current file's folder, if no folder is open in editor - const activeFile = historyService.getLastActiveFile(); + const activeFile = historyService.getLastActiveFile(Schemas.file); if (activeFile) { terminalService.openTerminal(paths.dirname(activeFile.fsPath)); } @@ -129,15 +133,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: OPEN_NATIVE_CONSOLE_COMMAND_ID, - title: env.isWindows ? nls.localize('globalConsoleActionWin', "Open New Command Prompt") : - nls.localize('globalConsoleActionMacLinux', "Open New Terminal") + title: nls.localize('globalConsoleAction', "Open New Terminal") } }); const openConsoleCommand = { id: OPEN_IN_TERMINAL_COMMAND_ID, - title: env.isWindows ? nls.localize('scopedConsoleActionWin', "Open in Command Prompt") : - nls.localize('scopedConsoleActionMacLinux', "Open in Terminal") + title: nls.localize('scopedConsoleAction', "Open in Terminal") }; MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', diff --git a/src/vs/workbench/parts/execution/electron-browser/terminal.ts b/src/vs/workbench/parts/execution/electron-browser/terminal.ts index c6dab0141ca..9017299073e 100644 --- a/src/vs/workbench/parts/execution/electron-browser/terminal.ts +++ b/src/vs/workbench/parts/execution/electron-browser/terminal.ts @@ -32,7 +32,7 @@ export function getDefaultTerminalLinuxReady(): TPromise { } c('xterm'); - }, () => { }); + }); } return _DEFAULT_TERMINAL_LINUX_READY; } diff --git a/src/vs/workbench/parts/execution/electron-browser/terminalService.ts b/src/vs/workbench/parts/execution/electron-browser/terminalService.ts index 275f11a662e..9327fcd7e78 100644 --- a/src/vs/workbench/parts/execution/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/execution/electron-browser/terminalService.ts @@ -9,14 +9,13 @@ import * as cp from 'child_process'; import * as path from 'path'; import * as processes from 'vs/base/node/processes'; import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITerminalService } from 'vs/workbench/parts/execution/common/execution'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalConfiguration, getDefaultTerminalWindows, getDefaultTerminalLinuxReady, DEFAULT_TERMINAL_OSX } from 'vs/workbench/parts/execution/electron-browser/terminal'; -import uri from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); @@ -38,8 +37,7 @@ export class WinTerminalService implements ITerminalService { public openTerminal(cwd?: string): void { const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd) - .done(null, errors.onUnexpectedError); + this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: IProcessEnvironment): TPromise { @@ -126,7 +124,7 @@ export class MacTerminalService implements ITerminalService { public openTerminal(cwd?: string): void { const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd).done(null, errors.onUnexpectedError); + this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: IProcessEnvironment): TPromise { @@ -143,7 +141,7 @@ export class MacTerminalService implements ITerminalService { // and then launches the program inside that window. const script = terminalApp === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper'; - const scriptpath = uri.parse(require.toUrl(`vs/workbench/parts/execution/electron-browser/${script}.scpt`)).fsPath; + const scriptpath = getPathFromAmdModule(require, `vs/workbench/parts/execution/electron-browser/${script}.scpt`); const osaArgs = [ scriptpath, @@ -218,8 +216,7 @@ export class LinuxTerminalService implements ITerminalService { public openTerminal(cwd?: string): void { const configuration = this._configurationService.getValue(); - this.spawnTerminal(cp, configuration, cwd) - .done(null, errors.onUnexpectedError); + this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: IProcessEnvironment): TPromise { diff --git a/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts b/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts new file mode 100644 index 00000000000..cee00f64590 --- /dev/null +++ b/src/vs/workbench/parts/experiments/electron-browser/experimentalPrompt.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties, IExperimentActionPromptCommand, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; + +export class ExperimentalPrompts extends Disposable implements IWorkbenchContribution { + private _disposables: IDisposable[] = []; + + constructor( + @IExperimentService private experimentService: IExperimentService, + @IViewletService private viewletService: IViewletService, + @INotificationService private notificationService: INotificationService, + @ITelemetryService private telemetryService: ITelemetryService + + ) { + super(); + this.experimentService.onExperimentEnabled(e => { + if (e.action && e.action.type === ExperimentActionType.Prompt && e.state === ExperimentState.Run) { + this.showExperimentalPrompts(e); + } + }, this, this._disposables); + } + + private showExperimentalPrompts(experiment: IExperiment): void { + if (!experiment || !experiment.enabled || !experiment.action || experiment.state !== ExperimentState.Run) { + return; + } + + const logTelemetry = (commandText?: string) => { + /* __GDPR__ + "experimentalPrompts" : { + "experimentId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "commandText": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "cancelled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('experimentalPrompts', { + experimentId: experiment.id, + commandText, + cancelled: !commandText + }); + }; + + const actionProperties = (experiment.action.properties); + if (!actionProperties || !actionProperties.promptText) { + return; + } + if (!actionProperties.commands) { + actionProperties.commands = []; + } + + const choices: IPromptChoice[] = actionProperties.commands.map((command: IExperimentActionPromptCommand) => { + return { + label: command.text, + run: () => { + logTelemetry(command.text); + if (command.externalLink) { + window.open(command.externalLink); + return; + } + if (command.curatedExtensionsKey && Array.isArray(command.curatedExtensionsList)) { + this.viewletService.openViewlet('workbench.view.extensions', true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + if (viewlet) { + viewlet.search('curated:' + command.curatedExtensionsKey); + } + }); + return; + } + + this.experimentService.markAsCompleted(experiment.id); + + } + }; + }); + + this.notificationService.prompt(Severity.Info, actionProperties.promptText, choices, logTelemetry); + } + + dispose() { + this._disposables = dispose(this._disposables); + } +} diff --git a/src/vs/workbench/parts/experiments/electron-browser/experiments.contribution.ts b/src/vs/workbench/parts/experiments/electron-browser/experiments.contribution.ts new file mode 100644 index 00000000000..93811199b24 --- /dev/null +++ b/src/vs/workbench/parts/experiments/electron-browser/experiments.contribution.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExperimentService, ExperimentService } from 'vs/workbench/parts/experiments/node/experimentService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ExperimentalPrompts } from 'vs/workbench/parts/experiments/electron-browser/experimentalPrompt'; + +registerSingleton(IExperimentService, ExperimentService); + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExperimentalPrompts, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/parts/experiments/node/experimentService.ts b/src/vs/workbench/parts/experiments/node/experimentService.ts new file mode 100644 index 00000000000..7ecb48803a4 --- /dev/null +++ b/src/vs/workbench/parts/experiments/node/experimentService.ts @@ -0,0 +1,435 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import product from 'vs/platform/node/product'; + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { language } from 'vs/base/common/platform'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { match } from 'vs/base/common/glob'; +import { asJson } from 'vs/base/node/request'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { WorkspaceStats } from 'vs/workbench/parts/stats/node/workspaceStats'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface IExperimentStorageState { + enabled: boolean; + state: ExperimentState; + editCount?: number; + lastEditedDate?: string; +} + +export const enum ExperimentState { + Evaluating, + NoRun, + Run, + Complete +} + +interface IRawExperiment { + id: string; + enabled?: boolean; + condition?: { + insidersOnly?: boolean; + displayLanguage?: string; + installedExtensions?: { + excludes?: string[]; + includes?: string[]; + }, + fileEdits?: { + filePathPattern?: string; + workspaceIncludes?: string[]; + workspaceExcludes?: string[]; + minEditCount: number; + }, + experimentsPreviouslyRun?: { + excludes?: string[]; + includes?: string[]; + } + userProbability?: number; + }; + action?: IExperimentAction; +} + +interface IExperimentAction { + type: ExperimentActionType; + properties: any; +} + +export enum ExperimentActionType { + Custom = 'Custom', + Prompt = 'Prompt', + AddToRecommendations = 'AddToRecommendations' +} + +export interface IExperimentActionPromptProperties { + promptText: string; + commands: IExperimentActionPromptCommand[]; +} + +export interface IExperimentActionPromptCommand { + text: string; + externalLink?: string; + curatedExtensionsKey?: string; + curatedExtensionsList?: string[]; +} + +export interface IExperiment { + id: string; + enabled: boolean; + state: ExperimentState; + action?: IExperimentAction; +} + +export interface IExperimentService { + _serviceBrand: any; + getExperimentById(id: string): TPromise; + getExperimentsByType(type: ExperimentActionType): TPromise; + getCuratedExtensionsList(curatedExtensionsKey: string): TPromise; + markAsCompleted(experimentId: string): void; + + onExperimentEnabled: Event; +} + +export const IExperimentService = createDecorator('experimentService'); + +export class ExperimentService extends Disposable implements IExperimentService { + _serviceBrand: any; + private _experiments: IExperiment[] = []; + private _loadExperimentsPromise: TPromise; + private _curatedMapping = Object.create(null); + private _disposables: IDisposable[] = []; + + private readonly _onExperimentEnabled: Emitter = new Emitter(); + + onExperimentEnabled: Event = this._onExperimentEnabled.event; + constructor( + @IStorageService private storageService: IStorageService, + @IExtensionManagementService private extensionManagementService: IExtensionManagementService, + @ITextFileService private textFileService: ITextFileService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ITelemetryService private telemetryService: ITelemetryService, + @ILifecycleService private lifecycleService: ILifecycleService, + @IRequestService private requestService: IRequestService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(); + + this._loadExperimentsPromise = TPromise.wrap(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); + } + + public getExperimentById(id: string): TPromise { + return this._loadExperimentsPromise.then(() => { + return this._experiments.filter(x => x.id === id)[0]; + }); + } + + public getExperimentsByType(type: ExperimentActionType): TPromise { + return this._loadExperimentsPromise.then(() => { + if (type === ExperimentActionType.Custom) { + return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); + } + return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); + }); + } + + public getCuratedExtensionsList(curatedExtensionsKey: string): TPromise { + return this._loadExperimentsPromise.then(() => { + for (let i = 0; i < this._experiments.length; i++) { + if (this._experiments[i].enabled + && this._experiments[i].state === ExperimentState.Run + && this._curatedMapping[this._experiments[i].id] + && this._curatedMapping[this._experiments[i].id].curatedExtensionsKey === curatedExtensionsKey) { + return this._curatedMapping[this._experiments[i].id].curatedExtensionsList; + } + } + return []; + }); + } + + public markAsCompleted(experimentId: string): void { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + experimentState.state = ExperimentState.Complete; + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + } + + protected getExperiments(): TPromise { + if (!product.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { + return TPromise.as([]); + } + return this.requestService.request({ type: 'GET', url: product.experimentsUrl }, CancellationToken.None).then(context => { + if (context.res.statusCode !== 200) { + return TPromise.as(null); + } + return asJson(context).then(result => { + return Array.isArray(result['experiments']) ? result['experiments'] : []; + }); + }, () => TPromise.as(null)); + } + + private loadExperiments(): TPromise { + return this.getExperiments().then(rawExperiments => { + // Offline mode + if (!rawExperiments) { + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experimentId => { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); + if (experimentState) { + this._experiments.push({ + id: experimentId, + enabled: experimentState.enabled, + state: experimentState.state + }); + } + }); + } + return TPromise.as(null); + } + + // Clear disbaled/deleted experiments from storage + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experiment => { + if (enabledExperiments.indexOf(experiment) === -1) { + this.storageService.remove('experiments.' + experiment); + } + }); + } + this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL); + + const promises = rawExperiments.map(experiment => { + const processedExperiment: IExperiment = { + id: experiment.id, + enabled: !!experiment.enabled, + state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun + }; + + if (experiment.action) { + processedExperiment.action = { + type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom, + properties: experiment.action.properties + }; + if (processedExperiment.action.type === ExperimentActionType.Prompt) { + ((processedExperiment.action.properties).commands || []).forEach(x => { + if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) { + this._curatedMapping[experiment.id] = x; + } + }); + } + } + this._experiments.push(processedExperiment); + + if (!processedExperiment.enabled) { + return TPromise.as(null); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (!experimentState.hasOwnProperty('enabled')) { + experimentState.enabled = processedExperiment.enabled; + } + if (!experimentState.hasOwnProperty('state')) { + experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun; + } else { + processedExperiment.state = experimentState.state; + } + + return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { + experimentState.state = processedExperiment.state = state; + this.storageService.store(storageKey, JSON.stringify(experimentState)); + + if (state === ExperimentState.Run) { + this.fireRunExperiment(processedExperiment); + } + return TPromise.as(null); + }); + + }); + return TPromise.join(promises).then(() => { + /* __GDPR__ + "experiments" : { + "experiments" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('experiments', { experiments: this._experiments }); + }); + }); + } + + private fireRunExperiment(experiment: IExperiment) { + this._onExperimentEnabled.fire(experiment); + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + if (runExperimentIdsFromStorage.indexOf(experiment.id)) { + runExperimentIdsFromStorage.push(experiment.id); + this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(runExperimentIdsFromStorage)); + } + } + + private checkExperimentDependencies(experiment: IRawExperiment): boolean { + if (experiment.condition.experimentsPreviouslyRun) { + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + let includeCheck = true; + let excludeCheck = true; + if (Array.isArray(experiment.condition.experimentsPreviouslyRun.includes)) { + includeCheck = runExperimentIdsFromStorage.some(x => experiment.condition.experimentsPreviouslyRun.includes.indexOf(x) > -1); + } + if (includeCheck && Array.isArray(experiment.condition.experimentsPreviouslyRun.excludes)) { + excludeCheck = !runExperimentIdsFromStorage.some(x => experiment.condition.experimentsPreviouslyRun.excludes.indexOf(x) > -1); + } + if (!includeCheck || !excludeCheck) { + return false; + } + } + return true; + } + + private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): TPromise { + if (processedExperiment.state !== ExperimentState.Evaluating) { + return TPromise.wrap(processedExperiment.state); + } + + if (!experiment.enabled) { + return TPromise.wrap(ExperimentState.NoRun); + } + + if (!experiment.condition) { + return TPromise.wrap(ExperimentState.Run); + } + + if (!this.checkExperimentDependencies(experiment)) { + return TPromise.wrap(ExperimentState.NoRun); + } + + if (this.environmentService.appQuality === 'stable' && experiment.condition.insidersOnly === true) { + return TPromise.wrap(ExperimentState.NoRun); + } + + if (typeof experiment.condition.displayLanguage === 'string') { + let localeToCheck = experiment.condition.displayLanguage.toLowerCase(); + let displayLanguage = language.toLowerCase(); + + if (localeToCheck !== displayLanguage) { + const a = displayLanguage.indexOf('-'); + const b = localeToCheck.indexOf('-'); + if (a > -1) { + displayLanguage = displayLanguage.substr(0, a); + } + if (b > -1) { + localeToCheck = localeToCheck.substr(0, b); + } + if (displayLanguage !== localeToCheck) { + return TPromise.wrap(ExperimentState.NoRun); + } + } + } + + if (!experiment.condition.userProbability) { + experiment.condition.userProbability = 1; + } + + let extensionsCheckPromise = TPromise.as(true); + if (experiment.condition.installedExtensions) { + extensionsCheckPromise = this.extensionManagementService.getInstalled(LocalExtensionType.User).then(locals => { + let includesCheck = true; + let excludesCheck = true; + const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`); + if (Array.isArray(experiment.condition.installedExtensions.includes) && experiment.condition.installedExtensions.includes.length) { + const extensionIncludes = experiment.condition.installedExtensions.includes.map(e => e.toLowerCase()); + includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1); + } + if (Array.isArray(experiment.condition.installedExtensions.excludes) && experiment.condition.installedExtensions.excludes.length) { + const extensionExcludes = experiment.condition.installedExtensions.excludes.map(e => e.toLowerCase()); + excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1); + } + return includesCheck && excludesCheck; + }); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + + return extensionsCheckPromise.then(success => { + if (!success || !experiment.condition.fileEdits || typeof experiment.condition.fileEdits.minEditCount !== 'number') { + const runExperiment = success && Math.random() < experiment.condition.userProbability; + return runExperiment ? ExperimentState.Run : ExperimentState.NoRun; + } + + experimentState.editCount = experimentState.editCount || 0; + if (experimentState.editCount >= experiment.condition.fileEdits.minEditCount) { + return ExperimentState.Run; + } + + const onSaveHandler = this.textFileService.models.onModelsSaved(e => { + const date = new Date().toDateString(); + const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (latestExperimentState.state !== ExperimentState.Evaluating) { + onSaveHandler.dispose(); + return; + } + e.forEach(event => { + if (event.kind !== StateChange.SAVED + || latestExperimentState.state !== ExperimentState.Evaluating + || date === latestExperimentState.lastEditedDate + || latestExperimentState.editCount >= experiment.condition.fileEdits.minEditCount) { + return; + } + let filePathCheck = true; + let workspaceCheck = true; + + if (typeof experiment.condition.fileEdits.filePathPattern === 'string') { + filePathCheck = match(experiment.condition.fileEdits.filePathPattern, event.resource.fsPath); + } + if (Array.isArray(experiment.condition.fileEdits.workspaceIncludes) && experiment.condition.fileEdits.workspaceIncludes.length) { + workspaceCheck = experiment.condition.fileEdits.workspaceIncludes.some(x => !!WorkspaceStats.tags[x]); + } + if (workspaceCheck && Array.isArray(experiment.condition.fileEdits.workspaceExcludes) && experiment.condition.fileEdits.workspaceExcludes.length) { + workspaceCheck = !experiment.condition.fileEdits.workspaceExcludes.some(x => !!WorkspaceStats.tags[x]); + } + if (filePathCheck && workspaceCheck) { + latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; + latestExperimentState.lastEditedDate = date; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + } + }); + if (latestExperimentState.editCount >= experiment.condition.fileEdits.minEditCount) { + processedExperiment.state = latestExperimentState.state = (Math.random() < experiment.condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + if (latestExperimentState.state === ExperimentState.Run && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { + this.fireRunExperiment(processedExperiment); + } + } + }); + this._disposables.push(onSaveHandler); + return ExperimentState.Evaluating; + }); + } + + dispose() { + this._disposables = dispose(this._disposables); + } +} + + +function safeParse(text: string, defaultObject: any) { + try { + return JSON.parse(text) || defaultObject; + } + catch (e) { + return defaultObject; + } +} diff --git a/src/vs/workbench/parts/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/parts/experiments/test/electron-browser/experimentalPrompts.test.ts new file mode 100644 index 00000000000..3edba678b81 --- /dev/null +++ b/src/vs/workbench/parts/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; + +import { IExperiment, ExperimentActionType, IExperimentService, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; + +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { Emitter } from 'vs/base/common/event'; +import { TestExperimentService } from 'vs/workbench/parts/experiments/test/node/experimentService.test'; +import { ExperimentalPrompts } from 'vs/workbench/parts/experiments/electron-browser/experimentalPrompt'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TPromise } from 'vs/base/common/winjs.base'; + +suite('Experimental Prompts', () => { + let instantiationService: TestInstantiationService; + let experimentService: TestExperimentService; + let experimentalPrompt: ExperimentalPrompts; + let onExperimentEnabledEvent: Emitter; + + let storageData = {}; + const promptText = 'Hello there! Can you see this?'; + const experiment: IExperiment = + { + id: 'experiment1', + enabled: true, + state: ExperimentState.Run, + action: { + type: ExperimentActionType.Prompt, + properties: { + promptText, + commands: [ + { + text: 'Yes', + dontShowAgain: true + }, + { + text: 'No' + } + ] + } + } + }; + + suiteSetup(() => { + instantiationService = new TestInstantiationService(); + + instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(ITelemetryService, NullTelemetryService); + + onExperimentEnabledEvent = new Emitter(); + + }); + + setup(() => { + storageData = {}; + instantiationService.stub(IStorageService, { + get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify(storageData) : c, + store: (a, b, c) => { + if (a === 'experiments.experiment1') { + storageData = JSON.parse(b); + } + } + }); + instantiationService.stub(INotificationService, new TestNotificationService()); + experimentService = instantiationService.createInstance(TestExperimentService); + experimentService.onExperimentEnabled = onExperimentEnabledEvent.event; + instantiationService.stub(IExperimentService, experimentService); + }); + + teardown(() => { + if (experimentService) { + experimentService.dispose(); + } + if (experimentalPrompt) { + experimentalPrompt.dispose(); + } + }); + + + test('Show experimental prompt if experiment should be run. Choosing an option should mark experiment as complete', () => { + + storageData = { + enabled: true, + state: ExperimentState.Run + }; + + instantiationService.stub(INotificationService, { + prompt: (a: Severity, b: string, c: IPromptChoice[], d) => { + assert.equal(b, promptText); + assert.equal(c.length, 2); + c[0].run(); + } + }); + + experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts); + onExperimentEnabledEvent.fire(experiment); + + return TPromise.as(null).then(result => { + assert.equal(storageData['state'], ExperimentState.Complete); + }); + + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts b/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts new file mode 100644 index 00000000000..0d6d97a54f8 --- /dev/null +++ b/src/vs/workbench/parts/experiments/test/node/experimentService.test.ts @@ -0,0 +1,713 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { ExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; +import { + IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, + IExtensionEnablementService, ILocalExtension, LocalExtensionType +} from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementService, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { Emitter } from 'vs/base/common/event'; +import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; +import { URLService } from 'vs/platform/url/common/urlService'; +import { IURLService } from 'vs/platform/url/common/url'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { assign } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; + +let experimentData = { + experiments: [] +}; + +const local = aLocalExtension('installedExtension1', { version: '1.0.0' }); + +function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension { + const localExtension = Object.create({ manifest: {} }); + assign(localExtension, { type: LocalExtensionType.User, manifest: {}, location: URI.file(`pub.${name}`) }, properties); + assign(localExtension.manifest, { name, publisher: 'pub', version: '1.0.0' }, manifest); + localExtension.identifier = { id: getLocalExtensionIdFromManifest(localExtension.manifest) }; + localExtension.metadata = { id: localExtension.identifier.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; + return localExtension; +} + +export class TestExperimentService extends ExperimentService { + public getExperiments(): TPromise { + return TPromise.wrap(experimentData.experiments); + } +} + +suite('Experiment Service', () => { + let instantiationService: TestInstantiationService; + let testConfigurationService: TestConfigurationService; + let testObject: ExperimentService; + let installEvent: Emitter, + didInstallEvent: Emitter, + uninstallEvent: Emitter, + didUninstallEvent: Emitter; + + suiteSetup(() => { + instantiationService = new TestInstantiationService(); + installEvent = new Emitter(); + didInstallEvent = new Emitter(); + uninstallEvent = new Emitter(); + didUninstallEvent = new Emitter(); + + instantiationService.stub(IExtensionManagementService, ExtensionManagementService); + instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); + instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); + instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); + instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(IURLService, URLService); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); + testConfigurationService = new TestConfigurationService(); + instantiationService.stub(IConfigurationService, testConfigurationService); + instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => c, store: () => { } }); + + setup(() => { + instantiationService.stub(IEnvironmentService, {}); + instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => c, store: () => { } }); + }); + + teardown(() => { + if (testObject) { + testObject.dispose(); + } + }); + }); + + test('Simple Experiment Test', () => { + experimentData = { + experiments: [ + { + id: 'experiment1' + }, + { + id: 'experiment2', + enabled: false + }, + { + id: 'experiment3', + enabled: true + }, + { + id: 'experiment4', + enabled: true, + condition: { + + } + }, + { + id: 'experiment5', + enabled: true, + condition: { + insidersOnly: true + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + const tests = []; + tests.push(testObject.getExperimentById('experiment1')); + tests.push(testObject.getExperimentById('experiment2')); + tests.push(testObject.getExperimentById('experiment3')); + tests.push(testObject.getExperimentById('experiment4')); + tests.push(testObject.getExperimentById('experiment5')); + + return TPromise.join(tests).then(results => { + assert.equal(results[0].id, 'experiment1'); + assert.equal(results[0].enabled, false); + assert.equal(results[0].state, ExperimentState.NoRun); + + assert.equal(results[1].id, 'experiment2'); + assert.equal(results[1].enabled, false); + assert.equal(results[1].state, ExperimentState.NoRun); + + assert.equal(results[2].id, 'experiment3'); + assert.equal(results[2].enabled, true); + assert.equal(results[2].state, ExperimentState.Run); + + assert.equal(results[3].id, 'experiment4'); + assert.equal(results[3].enabled, true); + assert.equal(results[3].state, ExperimentState.Run); + + assert.equal(results[4].id, 'experiment5'); + assert.equal(results[4].enabled, true); + assert.equal(results[4].state, ExperimentState.Run); + }); + }); + + test('Insiders only experiment shouldnt be enabled in stable', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + insidersOnly: true + } + } + ] + }; + + instantiationService.stub(IEnvironmentService, { appQuality: 'stable' }); + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.NoRun); + }); + }); + + test('Experiment with no matching display language should be disabled', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + displayLanguage: 'somethingthat-nooneknows' + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.NoRun); + }); + }); + + test('Experiment with condition type InstalledExtensions is enabled when one of the expected extensions is installed', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + installedExtensions: { + inlcudes: ['pub.installedExtension1', 'uninstalled-extention-id'] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Run); + }); + }); + + test('Experiment with condition type InstalledExtensions is disabled when none of the expected extensions is installed', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + installedExtensions: { + includes: ['uninstalled-extention-id1', 'uninstalled-extention-id2'] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.NoRun); + }); + }); + + test('Experiment with condition type InstalledExtensions is disabled when one of the exlcuded extensions is installed', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + installedExtensions: { + excludes: ['pub.installedExtension1', 'uninstalled-extention-id2'] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.NoRun); + }); + }); + + test('Experiment that is marked as complete should be disabled regardless of the conditions', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + installedExtensions: { + includes: ['pub.installedExtension1', 'uninstalled-extention-id2'] + } + } + } + ] + }; + + instantiationService.stub(IStorageService, { + get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify({ state: ExperimentState.Complete }) : c, + store: (a, b, c) => { } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Complete); + }); + }); + + test('Experiment with evaluate only once should read enablement from storage service', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + installedExtensions: { + excludes: ['pub.installedExtension1', 'uninstalled-extention-id2'] + }, + evaluateOnlyOnce: true + } + } + ] + }; + + instantiationService.stub(IStorageService, { + get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify({ enabled: true, state: ExperimentState.Run }) : c, + store: (a, b, c) => { } + }); + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Run); + }); + }); + + test('Curated list should be available if experiment is enabled.', () => { + const promptText = 'Hello there! Can you see this?'; + const curatedExtensionsKey = 'AzureDeploy'; + const curatedExtensionsList = ['uninstalled-extention-id1', 'uninstalled-extention-id2']; + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + action: { + type: 'Prompt', + properties: { + promptText, + commands: [ + { + text: 'Search Marketplace', + dontShowAgain: true, + curatedExtensionsKey, + curatedExtensionsList + }, + { + text: 'No' + } + ] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Run); + return testObject.getCuratedExtensionsList(curatedExtensionsKey).then(curatedList => { + assert.equal(curatedList, curatedExtensionsList); + }); + }); + }); + + test('Curated list shouldnt be available if experiment is disabled.', () => { + const promptText = 'Hello there! Can you see this?'; + const curatedExtensionsKey = 'AzureDeploy'; + const curatedExtensionsList = ['uninstalled-extention-id1', 'uninstalled-extention-id2']; + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: false, + action: { + type: 'Prompt', + properties: { + promptText, + commands: [ + { + text: 'Search Marketplace', + dontShowAgain: true, + curatedExtensionsKey, + curatedExtensionsList + }, + { + text: 'No' + } + ] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, false); + assert.equal(result.state, ExperimentState.NoRun); + return testObject.getCuratedExtensionsList(curatedExtensionsKey).then(curatedList => { + assert.equal(curatedList.length, 0); + }); + }); + }); + + test('Experiment that is disabled or deleted should be removed from storage', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: false + }, + { + id: 'experiment3', + enabled: true + } + ] + }; + + let storageDataExperiment1 = { enabled: false }; + let storageDataExperiment2 = { enabled: false }; + let storageDataAllExperiments = ['experiment1', 'experiment2', 'experiment3']; + instantiationService.stub(IStorageService, { + get: (a, b, c) => { + switch (a) { + case 'experiments.experiment1': + return JSON.stringify(storageDataExperiment1); + case 'experiments.experiment2': + return JSON.stringify(storageDataExperiment2); + case 'allExperiments': + return JSON.stringify(storageDataAllExperiments); + default: + break; + } + return c; + }, + store: (a, b, c) => { + switch (a) { + case 'experiments.experiment1': + storageDataExperiment1 = JSON.parse(b); + break; + case 'experiments.experiment2': + storageDataExperiment2 = JSON.parse(b); + break; + case 'allExperiments': + storageDataAllExperiments = JSON.parse(b); + break; + default: + break; + } + }, + remove: a => { + switch (a) { + case 'experiments.experiment1': + storageDataExperiment1 = null; + break; + case 'experiments.experiment2': + storageDataExperiment2 = null; + break; + case 'allExperiments': + storageDataAllExperiments = null; + break; + default: + break; + } + } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + const disabledExperiment = testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, false); + assert.equal(!!storageDataExperiment1, false); + }); + const deletedExperiment = testObject.getExperimentById('experiment2').then(result => { + assert.equal(!!result, false); + assert.equal(!!storageDataExperiment2, false); + }); + return TPromise.join([disabledExperiment, deletedExperiment]).then(() => { + assert.equal(storageDataAllExperiments.length, 1); + assert.equal(storageDataAllExperiments[0], 'experiment3'); + }); + + }); + + test('Offline mode', () => { + experimentData = { + experiments: null + }; + + let storageDataExperiment1 = { enabled: true, state: ExperimentState.Run }; + let storageDataExperiment2 = { enabled: true, state: ExperimentState.NoRun }; + let storageDataExperiment3 = { enabled: true, state: ExperimentState.Evaluating }; + let storageDataExperiment4 = { enabled: true, state: ExperimentState.Complete }; + let storageDataAllExperiments = ['experiment1', 'experiment2', 'experiment3', 'experiment4']; + instantiationService.stub(IStorageService, { + get: (a, b, c) => { + switch (a) { + case 'experiments.experiment1': + return JSON.stringify(storageDataExperiment1); + case 'experiments.experiment2': + return JSON.stringify(storageDataExperiment2); + case 'experiments.experiment3': + return JSON.stringify(storageDataExperiment3); + case 'experiments.experiment4': + return JSON.stringify(storageDataExperiment4); + case 'allExperiments': + return JSON.stringify(storageDataAllExperiments); + default: + break; + } + return c; + }, + store: (a, b, c) => { + switch (a) { + case 'experiments.experiment1': + storageDataExperiment1 = JSON.parse(b); + break; + case 'experiments.experiment2': + storageDataExperiment2 = JSON.parse(b); + break; + case 'experiments.experiment3': + storageDataExperiment3 = JSON.parse(b); + break; + case 'experiments.experiment4': + storageDataExperiment4 = JSON.parse(b); + break; + case 'allExperiments': + storageDataAllExperiments = JSON.parse(b); + break; + default: + break; + } + }, + remove: a => { + switch (a) { + case 'experiments.experiment1': + storageDataExperiment1 = null; + break; + case 'experiments.experiment2': + storageDataExperiment2 = null; + break; + case 'experiments.experiment3': + storageDataExperiment3 = null; + break; + case 'experiments.experiment4': + storageDataExperiment4 = null; + break; + case 'allExperiments': + storageDataAllExperiments = null; + break; + default: + break; + } + } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + + const tests = []; + tests.push(testObject.getExperimentById('experiment1')); + tests.push(testObject.getExperimentById('experiment2')); + tests.push(testObject.getExperimentById('experiment3')); + tests.push(testObject.getExperimentById('experiment4')); + + return TPromise.join(tests).then(results => { + assert.equal(results[0].id, 'experiment1'); + assert.equal(results[0].enabled, true); + assert.equal(results[0].state, ExperimentState.Run); + + assert.equal(results[1].id, 'experiment2'); + assert.equal(results[1].enabled, true); + assert.equal(results[1].state, ExperimentState.NoRun); + + assert.equal(results[2].id, 'experiment3'); + assert.equal(results[2].enabled, true); + assert.equal(results[2].state, ExperimentState.Evaluating); + + assert.equal(results[3].id, 'experiment4'); + assert.equal(results[3].enabled, true); + assert.equal(results[3].state, ExperimentState.Complete); + }); + + }); + + test('getExperimentByType', () => { + const customProperties = { + some: 'random-value' + }; + experimentData = { + experiments: [ + { + id: 'simple-experiment', + enabled: true + }, + { + id: 'custom-experiment', + enabled: true, + action: { + type: 'Custom', + properties: customProperties + } + }, + { + id: 'prompt-with-no-commands', + enabled: true, + action: { + type: 'Prompt', + properties: { + promptText: 'someText' + } + } + }, + { + id: 'prompt-with-commands', + enabled: true, + action: { + type: 'Prompt', + properties: { + promptText: 'someText', + commands: [ + { + text: 'Hello' + } + ] + } + } + } + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + const custom = testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => { + assert.equal(result.length, 2); + assert.equal(result[0].id, 'simple-experiment'); + assert.equal(result[1].id, 'custom-experiment'); + }); + const prompt = testObject.getExperimentsByType(ExperimentActionType.Prompt).then(result => { + assert.equal(result.length, 2); + assert.equal(result[0].id, 'prompt-with-no-commands'); + assert.equal(result[1].id, 'prompt-with-commands'); + }); + return TPromise.join([custom, prompt]); + }); + + test('experimentsPreviouslyRun includes, excludes check', () => { + experimentData = { + experiments: [ + { + id: 'experiment3', + enabled: true, + condition: { + experimentsPreviouslyRun: { + includes: ['experiment1'], + excludes: ['experiment2'] + } + } + }, + { + id: 'experiment4', + enabled: true, + condition: { + experimentsPreviouslyRun: { + includes: ['experiment1'], + excludes: ['experiment200'] + } + } + } + ] + }; + + let storageDataExperiment3 = { enabled: true, state: ExperimentState.Evaluating }; + let storageDataExperiment4 = { enabled: true, state: ExperimentState.Evaluating }; + instantiationService.stub(IStorageService, { + get: (a, b, c) => { + switch (a) { + case 'currentOrPreviouslyRunExperiments': + return JSON.stringify(['experiment1', 'experiment2']); + default: + break; + } + return c; + }, + store: (a, b, c) => { + switch (a) { + case 'experiments.experiment3': + storageDataExperiment3 = JSON.parse(b); + break; + case 'experiments.experiment4': + storageDataExperiment4 = JSON.parse(b); + break; + default: + break; + } + } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => { + assert.equal(result.length, 2); + assert.equal(result[0].id, 'experiment3'); + assert.equal(result[0].state, ExperimentState.NoRun); + assert.equal(result[1].id, 'experiment4'); + assert.equal(result[1].state, ExperimentState.Run); + assert.equal(storageDataExperiment3.state, ExperimentState.NoRun); + assert.equal(storageDataExperiment4.state, ExperimentState.Run); + return TPromise.as(null); + }); + }); + // test('Experiment with condition type FileEdit should increment editcount as appropriate', () => { + + // }); + + // test('Experiment with condition type WorkspaceEdit should increment editcount as appropriate', () => { + + // }); + + + +}); + + diff --git a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts b/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts deleted file mode 100644 index 0e0c2715ee8..00000000000 --- a/src/vs/workbench/parts/extensions/browser/dependenciesViewer.ts +++ /dev/null @@ -1,216 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise, Promise } from 'vs/base/common/winjs.base'; -import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; -import { Action } from 'vs/base/common/actions'; -import { IExtensionDependencies, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; -import { once } from 'vs/base/common/event'; -import { domEvent } from 'vs/base/browser/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - -export interface IExtensionTemplateData { - icon: HTMLImageElement; - name: HTMLElement; - identifier: HTMLElement; - author: HTMLElement; - extensionDisposables: IDisposable[]; - extensionDependencies: IExtensionDependencies; -} - -export interface IUnknownExtensionTemplateData { - identifier: HTMLElement; -} - -export class DataSource implements IDataSource { - - public getId(tree: ITree, element: IExtensionDependencies): string { - let id = element.identifier; - this.getParent(tree, element).then(parent => { - id = parent ? this.getId(tree, parent) + '/' + id : id; - }); - return id; - } - - public hasChildren(tree: ITree, element: IExtensionDependencies): boolean { - return element.hasDependencies; - } - - public getChildren(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependencies); - } - - public getParent(tree: ITree, element: IExtensionDependencies): Promise { - return TPromise.as(element.dependent); - } -} - -export class Renderer implements IRenderer { - - private static readonly EXTENSION_TEMPLATE_ID = 'extension-template'; - private static readonly UNKNOWN_EXTENSION_TEMPLATE_ID = 'unknown-extension-template'; - - constructor(@IInstantiationService private instantiationService: IInstantiationService) { - } - - public getHeight(tree: ITree, element: IExtensionDependencies): number { - return 62; - } - - public getTemplateId(tree: ITree, element: IExtensionDependencies): string { - return element.extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; - } - - public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { - if (Renderer.EXTENSION_TEMPLATE_ID === templateId) { - return this.renderExtensionTemplate(tree, container); - } - return this.renderUnknownExtensionTemplate(tree, container); - } - - private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData { - dom.addClass(container, 'dependency'); - - const icon = dom.append(container, dom.$('img.icon')); - const details = dom.append(container, dom.$('.details')); - - const header = dom.append(details, dom.$('.header')); - const name = dom.append(header, dom.$('span.name')); - const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); - const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => { - tree.setFocus(openExtensionAction.extensionDependencies); - tree.setSelection([openExtensionAction.extensionDependencies]); - openExtensionAction.run(e.ctrlKey || e.metaKey); - e.stopPropagation(); - e.preventDefault(); - })]; - const identifier = dom.append(header, dom.$('span.identifier')); - - const footer = dom.append(details, dom.$('.footer')); - const author = dom.append(footer, dom.$('.author')); - return { - icon, - name, - identifier, - author, - extensionDisposables, - set extensionDependencies(e: IExtensionDependencies) { - openExtensionAction.extensionDependencies = e; - } - }; - } - - private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData { - const messageContainer = dom.append(container, dom.$('div.unknown-dependency')); - dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); - dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Dependency', "Unknown Dependency:"); - - const identifier = dom.append(messageContainer, dom.$('span.message')); - return { identifier }; - } - - public renderElement(tree: ITree, element: IExtensionDependencies, templateId: string, templateData: any): void { - if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { - this.renderExtension(tree, element, templateData); - return; - } - this.renderUnknownExtension(tree, element, templateData); - } - - private renderExtension(tree: ITree, element: IExtensionDependencies, data: IExtensionTemplateData): void { - const extension = element.extension; - - const onError = once(domEvent(data.icon, 'error')); - onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); - data.icon.src = extension.iconUrl; - - if (!data.icon.complete) { - data.icon.style.visibility = 'hidden'; - data.icon.onload = () => data.icon.style.visibility = 'inherit'; - } else { - data.icon.style.visibility = 'inherit'; - } - - data.name.textContent = extension.displayName; - data.identifier.textContent = extension.id; - data.author.textContent = extension.publisherDisplayName; - data.extensionDependencies = element; - } - - private renderUnknownExtension(tree: ITree, element: IExtensionDependencies, data: IUnknownExtensionTemplateData): void { - data.identifier.textContent = element.identifier; - } - - public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { - if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { - templateData.extensionDisposables = dispose((templateData).extensionDisposables); - } - } -} - -export class Controller extends WorkbenchTreeController { - - constructor( - @IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService, - @IConfigurationService configurationService: IConfigurationService - ) { - super({}, configurationService); - - // TODO@Sandeep this should be a command - this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); - } - - protected onLeftClick(tree: ITree, element: IExtensionDependencies, event: IMouseEvent): boolean { - let currentFocused = tree.getFocus(); - if (super.onLeftClick(tree, element, event)) { - if (element.dependent === null) { - if (currentFocused) { - tree.setFocus(currentFocused); - } else { - tree.focusFirst(); - } - return true; - } - } - return false; - } - - public openExtension(tree: ITree, sideByside: boolean): boolean { - const element: IExtensionDependencies = tree.getFocus(); - if (element.extension) { - this.extensionsWorkdbenchService.open(element.extension, sideByside); - return true; - } - return false; - } -} - -class OpenExtensionAction extends Action { - - private _extensionDependencies: IExtensionDependencies; - - constructor(@IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService) { - super('extensions.action.openDependency', ''); - } - - public set extensionDependencies(extensionDependencies: IExtensionDependencies) { - this._extensionDependencies = extensionDependencies; - } - - public get extensionDependencies(): IExtensionDependencies { - return this._extensionDependencies; - } - - run(sideByside: boolean): TPromise { - return this.extensionsWorkdbenchService.open(this._extensionDependencies.extension, sideByside); - } -} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts deleted file mode 100644 index 6688b1125d7..00000000000 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ /dev/null @@ -1,2053 +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/extensionActions'; -import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IAction, Action } from 'vs/base/common/actions'; -import { Throttler } 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'; -import * as json from 'vs/base/common/json'; -import { ActionItem, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -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 } from 'vs/workbench/parts/extensions/common/extensions'; -import { ExtensionsConfigurationInitialContent } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate'; -import { LocalExtensionType, IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleViewletAction } from 'vs/workbench/browser/viewlet'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; -import { IFileService, IContent } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; -import { IExtensionService, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import URI from 'vs/base/common/uri'; -import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { Color } from 'vs/base/common/color'; -import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { PagedModel } from 'vs/base/common/paging'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; - -const promptDownloadManually = (extension: IExtension, message: string, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService) => { - notificationService.prompt(Severity.Error, message, [{ - label: localize('download', "Download Manually"), - run: () => openerService.open(URI.parse(extension.downloadUrl)).then(() => { - notificationService.prompt( - Severity.Info, - localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', extension.id), - [{ - label: InstallVSIXAction.LABEL, - run: () => { - const action = instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - action.run(); - action.dispose(); - } - }] - ); - }) - }]); -}; - -export class InstallAction extends Action { - - private static readonly InstallLabel = localize('installAction', "Install"); - private static readonly InstallingLabel = localize('installing', "Installing"); - - private static readonly Class = 'extension-action prominent install'; - private static readonly InstallingClass = 'extension-action install installing'; - - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService, - @IOpenerService private openerService: IOpenerService - ) { - super('extensions.install', InstallAction.InstallLabel, InstallAction.Class, false); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - if (!this.extension || this.extension.type === LocalExtensionType.System) { - this.enabled = false; - this.class = InstallAction.Class; - this.label = InstallAction.InstallLabel; - return; - } - - this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && this.extension.state === ExtensionState.Uninstalled; - - if (this.extension.state === ExtensionState.Installing) { - this.label = InstallAction.InstallingLabel; - this.class = InstallAction.InstallingClass; - this.tooltip = InstallAction.InstallingLabel; - } else { - this.label = InstallAction.InstallLabel; - this.class = InstallAction.Class; - this.tooltip = InstallAction.InstallLabel; - } - } - - run(): TPromise { - this.extensionsWorkbenchService.open(this.extension); - - return this.install(this.extension); - } - - private install(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.install(extension).then(null, err => { - if (!extension.downloadUrl) { - return this.notificationService.error(err); - } - - console.error(err); - - promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); - }); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class UninstallAction extends Action { - - private static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); - private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); - - private static readonly UninstallClass = 'extension-action uninstall'; - private static readonly UnInstallingClass = 'extension-action uninstall uninstalling'; - - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super('extensions.uninstall', UninstallAction.UninstallLabel, UninstallAction.UninstallClass, false); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - if (!this.extension) { - this.enabled = false; - return; - } - - const state = this.extension.state; - - if (state === ExtensionState.Uninstalling) { - this.label = UninstallAction.UninstallingLabel; - this.class = UninstallAction.UnInstallingClass; - this.enabled = false; - return; - } - - this.label = UninstallAction.UninstallLabel; - this.class = UninstallAction.UninstallClass; - - const installedExtensions = this.extensionsWorkbenchService.local.filter(e => e.id === this.extension.id); - - if (!installedExtensions.length) { - this.enabled = false; - return; - } - - if (installedExtensions[0].type !== LocalExtensionType.User) { - this.enabled = false; - return; - } - - this.enabled = true; - } - - run(): TPromise { - return this.extensionsWorkbenchService.uninstall(this.extension); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class CombinedInstallAction extends Action { - - private static readonly NoExtensionClass = 'extension-action prominent install no-extension'; - private installAction: InstallAction; - private uninstallAction: UninstallAction; - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { - this._extension = extension; - this.installAction.extension = extension; - this.uninstallAction.extension = extension; - } - - constructor( - @IInstantiationService instantiationService: IInstantiationService - ) { - super('extensions.combinedInstall', '', '', false); - - this.installAction = instantiationService.createInstance(InstallAction); - this.uninstallAction = instantiationService.createInstance(UninstallAction); - this.disposables.push(this.installAction, this.uninstallAction); - - this.installAction.onDidChange(this.update, this, this.disposables); - this.uninstallAction.onDidChange(this.update, this, this.disposables); - this.update(); - } - - private update(): void { - if (!this.extension || this.extension.type === LocalExtensionType.System) { - this.enabled = false; - this.class = CombinedInstallAction.NoExtensionClass; - } else if (this.installAction.enabled) { - this.enabled = true; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } else if (this.uninstallAction.enabled) { - this.enabled = true; - this.label = this.uninstallAction.label; - this.class = this.uninstallAction.class; - this.tooltip = this.uninstallAction.tooltip; - } else if (this.extension.state === ExtensionState.Installing) { - this.enabled = false; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } else if (this.extension.state === ExtensionState.Uninstalling) { - this.enabled = false; - this.label = this.uninstallAction.label; - this.class = this.uninstallAction.class; - this.tooltip = this.uninstallAction.tooltip; - } else { - this.enabled = false; - this.label = this.installAction.label; - this.class = this.installAction.class; - this.tooltip = this.installAction.tooltip; - } - } - - run(): TPromise { - if (this.installAction.enabled) { - return this.installAction.run(); - } else if (this.uninstallAction.enabled) { - return this.uninstallAction.run(); - } - - return TPromise.as(null); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class UpdateAction extends Action { - - private static readonly EnabledClass = 'extension-action prominent update'; - private static readonly DisabledClass = `${UpdateAction.EnabledClass} disabled`; - private static readonly Label = localize('updateAction', "Update"); - - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService, - @IOpenerService private openerService: IOpenerService - ) { - super('extensions.update', UpdateAction.Label, UpdateAction.DisabledClass, false); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - if (!this.extension) { - this.enabled = false; - this.class = UpdateAction.DisabledClass; - this.label = UpdateAction.Label; - return; - } - - if (this.extension.type !== LocalExtensionType.User) { - this.enabled = false; - this.class = UpdateAction.DisabledClass; - this.label = UpdateAction.Label; - return; - } - - const canInstall = this.extensionsWorkbenchService.canInstall(this.extension); - const isInstalled = this.extension.state === ExtensionState.Installed; - - this.enabled = canInstall && isInstalled && this.extension.outdated; - this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; - this.label = localize('updateTo', "Update to {0}", this.extension.latestVersion); - } - - run(): TPromise { - return this.install(this.extension); - } - - private install(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.install(extension).then(null, err => { - if (!extension.downloadUrl) { - return this.notificationService.error(err); - } - - console.error(err); - - promptDownloadManually(extension, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); - }); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export interface IExtensionAction extends IAction { - extension: IExtension; -} - -export class DropDownMenuActionItem extends ActionItem { - - private disposables: IDisposable[] = []; - private _extension: IExtension; - - constructor(action: IAction, private menuActionGroups: IExtensionAction[][], - @IContextMenuService private contextMenuService: IContextMenuService - ) { - super(null, action, { icon: true, label: true }); - for (const menuActions of menuActionGroups) { - this.disposables = [...this.disposables, ...menuActions]; - } - } - - get extension(): IExtension { return this._extension; } - - set extension(extension: IExtension) { - this._extension = extension; - for (const menuActions of this.menuActionGroups) { - for (const menuAction of menuActions) { - menuAction.extension = extension; - } - } - } - - public showMenu(): void { - const actions = this.getActions(); - let elementPosition = DOM.getDomNodePagePosition(this.builder.getHTMLElement()); - const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => TPromise.wrap(actions), - actionRunner: this.actionRunner - }); - } - - private getActions(): IAction[] { - let actions: IAction[] = []; - const menuActionGroups = this.menuActionGroups; - for (const menuActions of menuActionGroups) { - actions = [...actions, ...menuActions, new Separator()]; - } - return actions.length ? actions.slice(0, actions.length - 1) : actions; - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class ManageExtensionAction extends Action { - - static readonly ID = 'extensions.manage'; - - private static readonly Class = 'extension-action manage'; - private static readonly HideManageExtensionClass = `${ManageExtensionAction.Class} hide`; - - private _actionItem: DropDownMenuActionItem; - get actionItem(): IActionItem { return this._actionItem; } - - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this._actionItem.extension = extension; this.update(); } - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super(ManageExtensionAction.ID); - - this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, [ - [ - instantiationService.createInstance(EnableGloballyAction, EnableGloballyAction.LABEL), - instantiationService.createInstance(EnableForWorkspaceAction, EnableForWorkspaceAction.LABEL) - ], - [ - instantiationService.createInstance(DisableGloballyAction, DisableGloballyAction.LABEL), - instantiationService.createInstance(DisableForWorkspaceAction, DisableForWorkspaceAction.LABEL) - ], - [ - instantiationService.createInstance(UninstallAction) - ] - ]); - this.disposables.push(this._actionItem); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - this.class = ManageExtensionAction.HideManageExtensionClass; - this.tooltip = ''; - this.enabled = false; - if (this.extension) { - const state = this.extension.state; - this.enabled = state === ExtensionState.Installed; - this.class = this.enabled || state === ExtensionState.Uninstalling ? ManageExtensionAction.Class : ManageExtensionAction.HideManageExtensionClass; - this.tooltip = state === ExtensionState.Uninstalling ? localize('ManageExtensionAction.uninstallingTooltip', "Uninstalling") : ''; - } - } - - public run(): TPromise { - this._actionItem.showMenu(); - return TPromise.wrap(null); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class EnableForWorkspaceAction extends Action implements IExtensionAction { - - static readonly ID = 'extensions.enableForWorkspace'; - static LABEL = localize('enableForWorkspaceAction', "Enable (Workspace)"); - - private disposables: IDisposable[] = []; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor(label: string, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(EnableForWorkspaceAction.ID, label); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.disposables.push(this.workspaceContextService.onDidChangeWorkbenchState(() => this.update())); - this.update(); - } - - private update(): void { - this.enabled = false; - if (this.extension) { - this.enabled = (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); - } - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.WorkspaceEnabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class EnableGloballyAction extends Action implements IExtensionAction { - - static readonly ID = 'extensions.enableGlobally'; - static LABEL = localize('enableGloballyAction', "Enable"); - - private disposables: IDisposable[] = []; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor(label: string, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(EnableGloballyAction.ID, label); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - this.enabled = false; - if (this.extension) { - this.enabled = (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); - } - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.Enabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class EnableAction extends Action { - - static readonly ID = 'extensions.enable'; - private static readonly EnabledClass = 'extension-action prominent enable'; - private static readonly DisabledClass = `${EnableAction.EnabledClass} disabled`; - - private disposables: IDisposable[] = []; - - private _enableActions: IExtensionAction[]; - - private _actionItem: DropDownMenuActionItem; - get actionItem(): IActionItem { return this._actionItem; } - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this._actionItem.extension = extension; this.update(); } - - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(EnableAction.ID, localize('enableAction', "Enable"), EnableAction.DisabledClass, false); - - this._enableActions = [ - instantiationService.createInstance(EnableGloballyAction, EnableGloballyAction.LABEL), - instantiationService.createInstance(EnableForWorkspaceAction, EnableForWorkspaceAction.LABEL) - ]; - this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, [this._enableActions]); - this.disposables.push(this._actionItem); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - if (!this.extension) { - this.enabled = false; - this.class = EnableAction.DisabledClass; - return; - } - - this.enabled = this.extension.state === ExtensionState.Installed && this._enableActions.some(e => e.enabled); - this.class = this.enabled ? EnableAction.EnabledClass : EnableAction.DisabledClass; - } - - public run(): TPromise { - this._actionItem.showMenu(); - return TPromise.wrap(null); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } - -} - -export class DisableForWorkspaceAction extends Action implements IExtensionAction { - - static readonly ID = 'extensions.disableForWorkspace'; - static LABEL = localize('disableForWorkspaceAction', "Disable (Workspace)"); - - private disposables: IDisposable[] = []; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor(label: string, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(DisableForWorkspaceAction.ID, label); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); - } - - private update(): void { - this.enabled = false; - if (this.extension && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) { - this.enabled = (this.extension.enablementState === EnablementState.Enabled || this.extension.enablementState === EnablementState.WorkspaceEnabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); - } - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.WorkspaceDisabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class DisableGloballyAction extends Action implements IExtensionAction { - - static readonly ID = 'extensions.disableGlobally'; - static LABEL = localize('disableGloballyAction', "Disable"); - - private disposables: IDisposable[] = []; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor(label: string, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(DisableGloballyAction.ID, label); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - this.enabled = false; - if (this.extension) { - this.enabled = (this.extension.enablementState === EnablementState.Enabled || this.extension.enablementState === EnablementState.WorkspaceEnabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); - } - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.Disabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class DisableAction extends Action { - - static readonly ID = 'extensions.disable'; - - private static readonly EnabledClass = 'extension-action disable'; - private static readonly DisabledClass = `${DisableAction.EnabledClass} disabled`; - - private disposables: IDisposable[] = []; - private _disableActions: IExtensionAction[]; - private _actionItem: DropDownMenuActionItem; - get actionItem(): IActionItem { return this._actionItem; } - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this._actionItem.extension = extension; this.update(); } - - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - ) { - super(DisableAction.ID, localize('disableAction', "Disable"), DisableAction.DisabledClass, false); - this._disableActions = [ - instantiationService.createInstance(DisableGloballyAction, DisableGloballyAction.LABEL), - instantiationService.createInstance(DisableForWorkspaceAction, DisableForWorkspaceAction.LABEL) - ]; - this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, [this._disableActions]); - this.disposables.push(this._actionItem); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - if (!this.extension) { - this.enabled = false; - this.class = DisableAction.DisabledClass; - return; - } - - this.enabled = this.extension.state === ExtensionState.Installed && this._disableActions.some(a => a.enabled); - this.class = this.enabled ? DisableAction.EnabledClass : DisableAction.DisabledClass; - } - - public run(): TPromise { - this._actionItem.showMenu(); - return TPromise.wrap(null); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class CheckForUpdatesAction extends Action { - - static readonly ID = 'workbench.extensions.action.checkForUpdates'; - static LABEL = localize('checkForUpdates', "Check for Updates"); - - constructor( - id = UpdateAllAction.ID, - label = UpdateAllAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(id, label, '', true); - } - - run(): TPromise { - return this.extensionsWorkbenchService.checkForUpdates(); - } -} - -export class ToggleAutoUpdateAction extends Action { - - constructor( - id: string, - label: string, - private autoUpdateValue: boolean, - @IConfigurationService private configurationService: IConfigurationService - ) { - super(id, label, '', true); - this.updateEnablement(); - configurationService.onDidChangeConfiguration(() => this.updateEnablement()); - } - - private updateEnablement(): void { - this.enabled = this.configurationService.getValue(AutoUpdateConfigurationKey) !== this.autoUpdateValue; - } - - run(): TPromise { - return this.configurationService.updateValue(AutoUpdateConfigurationKey, this.autoUpdateValue); - } -} - -export class EnableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.enableAutoUpdate'; - static LABEL = localize('enableAutoUpdate', "Enable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, true, configurationService); - } -} - -export class DisableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.disableAutoUpdate'; - static LABEL = localize('disableAutoUpdate', "Disable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, false, configurationService); - } -} - -export class UpdateAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.updateAllExtensions'; - static LABEL = localize('updateAll', "Update All Extensions"); - - private disposables: IDisposable[] = []; - - constructor( - id = UpdateAllAction.ID, - label = UpdateAllAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService, - @IOpenerService private openerService: IOpenerService - ) { - super(id, label, '', false); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private get outdated(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => e.outdated && e.state !== ExtensionState.Installing); - } - - private update(): void { - this.enabled = this.outdated.length > 0; - } - - run(): TPromise { - return TPromise.join(this.outdated.map(e => this.install(e))); - } - - private install(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.install(extension).then(null, err => { - if (!extension.downloadUrl) { - return this.notificationService.error(err); - } - - console.error(err); - - promptDownloadManually(extension, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); - }); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class ReloadAction extends Action { - - private static readonly EnabledClass = 'extension-action reload'; - private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; - - private disposables: IDisposable[] = []; - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - reloadMessage: string = ''; - private throttler: Throttler; - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWindowService private windowService: IWindowService, - @IExtensionService private extensionService: IExtensionService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, - ) { - super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); - this.throttler = new Throttler(); - - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - this.throttler.queue(() => { - this.enabled = false; - this.tooltip = ''; - this.reloadMessage = ''; - if (!this.extension) { - return TPromise.wrap(null); - } - const state = this.extension.state; - if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { - return TPromise.wrap(null); - } - return this.extensionService.getExtensions() - .then(runningExtensions => this.computeReloadState(runningExtensions)); - }).done(() => { - this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; - }); - } - - private computeReloadState(runningExtensions: IExtensionDescription[]): void { - const isInstalled = this.extensionsWorkbenchService.local.some(e => e.id === this.extension.id); - const isUninstalled = this.extension.state === ExtensionState.Uninstalled; - const isDisabled = this.extension.local ? !this.extensionEnablementService.isEnabled(this.extension.local) : false; - - const filteredExtensions = runningExtensions.filter(e => areSameExtensions(e, this.extension)); - const isExtensionRunning = filteredExtensions.length > 0; - const isDifferentVersionRunning = filteredExtensions.length > 0 && this.extension.version !== filteredExtensions[0].version; - - if (isInstalled) { - if (isDifferentVersionRunning && !isDisabled) { - // Requires reload to run the updated extension - this.enabled = true; - this.tooltip = localize('postUpdateTooltip', "Reload to update"); - this.reloadMessage = localize('postUpdateMessage', "Reload this window to activate the updated extension '{0}'?", this.extension.displayName); - return; - } - - if (!isExtensionRunning && !isDisabled) { - // Requires reload to enable the extension - this.enabled = true; - this.tooltip = localize('postEnableTooltip', "Reload to activate"); - this.reloadMessage = localize('postEnableMessage', "Reload this window to activate the extension '{0}'?", this.extension.displayName); - return; - } - - if (isExtensionRunning && isDisabled) { - // Requires reload to disable the extension - this.enabled = true; - this.tooltip = localize('postDisableTooltip', "Reload to deactivate"); - this.reloadMessage = localize('postDisableMessage', "Reload this window to deactivate the extension '{0}'?", this.extension.displayName); - return; - } - return; - } - - if (isUninstalled && isExtensionRunning) { - // Requires reload to deactivate the extension - this.enabled = true; - this.tooltip = localize('postUninstallTooltip', "Reload to deactivate"); - this.reloadMessage = localize('postUninstallMessage', "Reload this window to deactivate the uninstalled extension '{0}'?", this.extension.displayName); - return; - } - } - - run(): TPromise { - return this.windowService.reloadWindow(); - } -} - -export class OpenExtensionsViewletAction extends ToggleViewletAction { - - static ID = VIEWLET_ID; - static LABEL = localize('toggleExtensionsViewlet', "Show Extensions"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService); - } -} - -export class InstallExtensionsAction extends OpenExtensionsViewletAction { - static ID = 'workbench.extensions.action.installExtensions'; - static LABEL = localize('installExtensions', "Install Extensions"); -} - -export class ShowEnabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showEnabledExtensions'; - static LABEL = localize('showEnabledExtensions', 'Show Enabled Extensions'); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, 'clear-extensions', true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@enabled '); - viewlet.focus(); - }); - } -} - -export class ShowInstalledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showInstalledExtensions'; - static LABEL = localize('showInstalledExtensions', "Show Installed Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, 'clear-extensions', true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@installed '); - viewlet.focus(); - }); - } -} - -export class ShowDisabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showDisabledExtensions'; - static LABEL = localize('showDisabledExtensions', "Show Disabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, 'null', true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@disabled '); - viewlet.focus(); - }); - } -} - -export class ClearExtensionsInputAction extends Action { - - static readonly ID = 'workbench.extensions.action.clearExtensionsInput'; - static LABEL = localize('clearExtensionsInput', "Clear Extensions Input"); - - private disposables: IDisposable[] = []; - - constructor( - id: string, - label: string, - onSearchChange: Event, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, 'clear-extensions', true); - this.enabled = false; - onSearchChange(this.onSearchChange, this, this.disposables); - } - - private onSearchChange(value: string): void { - this.enabled = !!value; - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search(''); - viewlet.focus(); - }); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - } -} - -export class ShowBuiltInExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listBuiltInExtensions'; - static LABEL = localize('showBuiltInExtensions', "Show Built-in Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@builtin '); - viewlet.focus(); - }); - } -} - -export class ShowOutdatedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listOutdatedExtensions'; - static LABEL = localize('showOutdatedExtensions', "Show Outdated Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@outdated '); - viewlet.focus(); - }); - } -} - -export class ShowPopularExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showPopularExtensions'; - static LABEL = localize('showPopularExtensions', "Show Popular Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@sort:installs '); - viewlet.focus(); - }); - } -} - -export class ShowRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; - static LABEL = localize('showRecommendedExtensions', "Show Recommended Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - }); - } -} - -export class InstallWorkspaceRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions'; - static LABEL = localize('installWorkspaceRecommendedExtensions', "Install All Workspace Recommended Extensions"); - - private disposables: IDisposable[] = []; - - constructor( - id: string = InstallWorkspaceRecommendedExtensionsAction.ID, - label: string = InstallWorkspaceRecommendedExtensionsAction.LABEL, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IViewletService private viewletService: IViewletService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionTipsService private extensionTipsService: IExtensionTipsService, - @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService, - @IOpenerService private openerService: IOpenerService - ) { - super(id, label, 'extension-action'); - this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); - this.contextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); - } - - private update(): void { - this.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; - if (this.enabled) { - this.extensionTipsService.getWorkspaceRecommendations().then(names => { - const installed = this.extensionsWorkbenchService.local.map(x => x.id.toLowerCase()); - this.enabled = names.some(x => installed.indexOf(x.toLowerCase()) === -1); - }); - } - } - - run(): TPromise { - return this.extensionTipsService.getWorkspaceRecommendations().then(names => { - const installed = this.extensionsWorkbenchService.local.map(x => x.id.toLowerCase()); - const toInstall = names.filter(x => installed.indexOf(x.toLowerCase()) === -1); - - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - if (!toInstall.length) { - this.enabled = false; - this.notificationService.info(localize('allExtensionsInstalled', "All extensions recommended for this workspace have already been installed")); - viewlet.focus(); - return TPromise.as(null); - } - - viewlet.search('@recommended '); - viewlet.focus(); - - return this.extensionsWorkbenchService.queryGallery({ names: toInstall, source: 'install-all-workspace-recommendations' }).then(pager => { - let installPromises = []; - let model = new PagedModel(pager); - let extensionsWithDependencies = []; - for (let i = 0; i < pager.total; i++) { - installPromises.push(model.resolve(i).then(e => { - if (e.dependencies && e.dependencies.length > 0) { - extensionsWithDependencies.push(e); - return TPromise.as(null); - } else { - return this.install(e); - } - })); - } - return TPromise.join(installPromises).then(() => { - return TPromise.join(extensionsWithDependencies.map(e => this.install(e))); - }); - }); - }); - }); - } - - private install(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.install(extension).then(null, err => { - if (!extension.downloadUrl) { - return this.notificationService.error(err); - } - - console.error(err); - - promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); - }); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - super.dispose(); - } -} - -export class InstallRecommendedExtensionAction extends Action { - - static readonly ID = 'workbench.extensions.action.installRecommendedExtension'; - static LABEL = localize('installRecommendedExtension', "Install Recommended Extension"); - - private extensionId: string; - private disposables: IDisposable[] = []; - - constructor( - extensionId: string, - @IViewletService private viewletService: IViewletService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService private notificationService: INotificationService, - @IInstantiationService private instantiationService: IInstantiationService, - @IOpenerService private openerService: IOpenerService - ) { - super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, null); - this.extensionId = extensionId; - this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); - } - - private update(): void { - this.enabled = !this.extensionsWorkbenchService.local.some(x => x.id.toLowerCase() === this.extensionId.toLowerCase()); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - if (this.extensionsWorkbenchService.local.some(x => x.id.toLowerCase() === this.extensionId.toLowerCase())) { - this.enabled = false; - this.notificationService.info(localize('extensionInstalled', "The recommended extension has already been installed")); - viewlet.focus(); - return TPromise.as(null); - } - - viewlet.search('@recommended '); - viewlet.focus(); - - return this.extensionsWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation' }).then(pager => { - return (pager && pager.firstPage && pager.firstPage.length) ? this.install(pager.firstPage[0]) : TPromise.as(null); - }); - }); - } - - private install(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.install(extension).then(null, err => { - if (!extension.downloadUrl) { - return this.notificationService.error(err); - } - - console.error(err); - - promptDownloadManually(extension, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); - }); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - super.dispose(); - } -} - - -export class ShowRecommendedKeymapExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; - static SHORT_LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@recommended:keymaps '); - viewlet.focus(); - }); - } -} - -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showLanguageExtensions'; - static SHORT_LABEL = localize('showLanguageExtensionsShort', "Language Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@sort:installs category:languages '); - viewlet.focus(); - }); - } -} - -export class ShowAzureExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showAzureExtensions'; - static SHORT_LABEL = localize('showAzureExtensionsShort', "Azure Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - } - - run(): TPromise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('@sort:installs azure '); - viewlet.focus(); - }); - } -} - -export class ChangeSortAction extends Action { - - private query: Query; - private disposables: IDisposable[] = []; - - constructor( - id: string, - label: string, - onSearchChange: Event, - private sortBy: string, - @IViewletService private viewletService: IViewletService - ) { - super(id, label, null, true); - - if (sortBy === undefined) { - throw new Error('bad arguments'); - } - - this.query = Query.parse(''); - this.enabled = false; - onSearchChange(this.onSearchChange, this, this.disposables); - } - - private onSearchChange(value: string): void { - const query = Query.parse(value); - this.query = new Query(query.value, this.sortBy || query.sortBy); - this.enabled = value && this.query.isValid() && !this.query.equals(query); - } - - run(): TPromise { - 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); - private workspaceFolderContextKey = new RawContextKey('workspaceFolderRecommendations', true); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService - ) { - super(); - const boundWorkspaceContextKey = this.workspaceContextKey.bindTo(contextKeyService); - boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE); - this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); - - - const boundWorkspaceFolderContextKey = this.workspaceFolderContextKey.bindTo(contextKeyService); - boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0); - this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0))); - - this.registerCommands(); - } - - private registerCommands(): void { - CommandsRegistry.registerCommand(ConfigureWorkspaceRecommendedExtensionsAction.ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: `${ExtensionsLabel}: ${ConfigureWorkspaceRecommendedExtensionsAction.LABEL}`, - }, - when: this.workspaceContextKey - }); - - CommandsRegistry.registerCommand(ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, serviceAccessor => { - serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); - }); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: `${ExtensionsLabel}: ${ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL}`, - }, - when: this.workspaceFolderContextKey - }); - } -} - -interface IExtensionsContent { - recommendations: string[]; -} - -export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { - - constructor( - id: string, - label: string, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IFileService private fileService: IFileService, - @IEditorService private editorService: IEditorService, - @IJSONEditingService private jsonEditingService: IJSONEditingService, - @ITextModelService private textModelResolverService: ITextModelService - ) { - super(id, label, null); - } - - protected openExtensionsFile(extensionsFileResource: URI): TPromise { - return this.getOrCreateExtensionsFile(extensionsFileResource) - .then(({ created, content }) => - this.getSelectionPosition(content, extensionsFileResource, ['recommendations']) - .then(selection => this.editorService.openEditor({ - resource: extensionsFileResource, - options: { - forceOpen: true, - pinned: created, - selection - } - })), - error => TPromise.wrapError(new Error(localize('OpenExtensionsFile.failed', "Unable to create 'extensions.json' file inside the '.vscode' folder ({0}).", error)))); - } - - protected openWorkspaceConfigurationFile(workspaceConfigurationFile: URI): TPromise { - return this.getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile) - .then(content => this.getSelectionPosition(content.value, content.resource, ['extensions', 'recommendations'])) - .then(selection => this.editorService.openEditor({ - resource: workspaceConfigurationFile, - options: { - forceOpen: true, - selection - } - })); - } - - private getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile: URI): TPromise { - return this.fileService.resolveContent(workspaceConfigurationFile) - .then(content => { - const workspaceRecommendations = json.parse(content.value)['extensions']; - if (!workspaceRecommendations || !workspaceRecommendations.recommendations) { - return this.jsonEditingService.write(workspaceConfigurationFile, { key: 'extensions', value: { recommendations: [] } }, true) - .then(() => this.fileService.resolveContent(workspaceConfigurationFile)); - } - return content; - }); - } - - private getSelectionPosition(content: string, resource: URI, path: json.JSONPath): TPromise { - const tree = json.parseTree(content); - const node = json.findNodeAtLocation(tree, path); - if (node && node.parent.children[1]) { - const recommendationsValueNode = node.parent.children[1]; - const lastExtensionNode = recommendationsValueNode.children && recommendationsValueNode.children.length ? recommendationsValueNode.children[recommendationsValueNode.children.length - 1] : null; - const offset = lastExtensionNode ? lastExtensionNode.offset + lastExtensionNode.length : recommendationsValueNode.offset + 1; - return this.textModelResolverService.createModelReference(resource) - .then(reference => { - const position = reference.object.textEditorModel.getPositionAt(offset); - reference.dispose(); - return { - startLineNumber: position.lineNumber, - startColumn: position.column, - endLineNumber: position.lineNumber, - endColumn: position.column, - }; - }); - } - return TPromise.as(null); - } - - private getOrCreateExtensionsFile(extensionsFileResource: URI): TPromise<{ created: boolean, extensionsFileResource: URI, content: string }> { - return this.fileService.resolveContent(extensionsFileResource).then(content => { - return { created: false, extensionsFileResource, content: content.value }; - }, err => { - return this.fileService.updateContent(extensionsFileResource, ExtensionsConfigurationInitialContent).then(() => { - return { created: true, extensionsFileResource, content: ExtensionsConfigurationInitialContent }; - }); - }); - } -} - -export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfigureRecommendedExtensionsAction { - - static readonly ID = 'workbench.extensions.action.configureWorkspaceRecommendedExtensions'; - static LABEL = localize('configureWorkspaceRecommendedExtensions', "Configure Recommended Extensions (Workspace)"); - - private disposables: IDisposable[] = []; - - constructor( - id: string, - label: string, - @IFileService fileService: IFileService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IEditorService editorService: IEditorService, - @IJSONEditingService jsonEditingService: IJSONEditingService, - @ITextModelService textModelResolverService: ITextModelService - ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); - this.contextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); - this.update(); - } - - private update(): void { - this.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; - } - - public run(): TPromise { - switch (this.contextService.getWorkbenchState()) { - case WorkbenchState.FOLDER: - return this.openExtensionsFile(this.contextService.getWorkspace().folders[0].toResource(paths.join('.vscode', 'extensions.json'))); - case WorkbenchState.WORKSPACE: - return this.openWorkspaceConfigurationFile(this.contextService.getWorkspace().configuration); - } - return TPromise.as(null); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - super.dispose(); - } -} - -export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends AbstractConfigureRecommendedExtensionsAction { - - static readonly ID = 'workbench.extensions.action.configureWorkspaceFolderRecommendedExtensions'; - static LABEL = localize('configureWorkspaceFolderRecommendedExtensions', "Configure Recommended Extensions (Workspace Folder)"); - - private disposables: IDisposable[] = []; - - constructor( - id: string, - label: string, - @IFileService fileService: IFileService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IEditorService editorService: IEditorService, - @IJSONEditingService jsonEditingService: IJSONEditingService, - @ITextModelService textModelResolverService: ITextModelService, - @ICommandService private commandService: ICommandService - ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); - this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this, this.disposables); - this.update(); - } - - private update(): void { - this.enabled = this.contextService.getWorkspace().folders.length > 0; - } - - public run(): TPromise { - const folderCount = this.contextService.getWorkspace().folders.length; - const pickFolderPromise = folderCount === 1 ? TPromise.as(this.contextService.getWorkspace().folders[0]) : this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); - return pickFolderPromise - .then(workspaceFolder => { - if (workspaceFolder) { - return this.openExtensionsFile(workspaceFolder.toResource(paths.join('.vscode', 'extensions.json'))); - } - return null; - }); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - super.dispose(); - } -} - -export class MaliciousStatusLabelAction extends Action { - - private static readonly Class = 'malicious-status'; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - constructor(long: boolean) { - const tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); - const label = long ? tooltip : localize('malicious', "Malicious"); - super('extensions.install', label, '', false); - this.tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); - } - - private update(): void { - if (this.extension && this.extension.isMalicious) { - this.class = `${MaliciousStatusLabelAction.Class} malicious`; - } else { - this.class = `${MaliciousStatusLabelAction.Class} not-malicious`; - } - } - - run(): TPromise { - return TPromise.as(null); - } -} - -export class DisabledStatusLabelAction extends Action { - - private static readonly Class = 'disable-status'; - - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } - - private disposables: IDisposable[] = []; - private throttler: Throttler = new Throttler(); - - constructor( - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionService private extensionService: IExtensionService - ) { - super('extensions.install', localize('disabled', "Disabled"), `${DisabledStatusLabelAction.Class} hide`, false); - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - this.update(); - } - - private update(): void { - this.throttler.queue(() => this.extensionService.getExtensions() - .then(runningExtensions => { - this.class = `${DisabledStatusLabelAction.Class} hide`; - this.tooltip = ''; - if (this.extension && !this.extension.isMalicious && !runningExtensions.some(e => e.id === this.extension.id)) { - if (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) { - this.class = `${DisabledStatusLabelAction.Class}`; - this.tooltip = this.extension.enablementState === EnablementState.Disabled ? localize('disabled globally', "Disabled") : localize('disabled workspace', "Disabled for this Workspace"); - } - } - })); - } - - run(): TPromise { - return TPromise.as(null); - } -} - -export class DisableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAll'; - static LABEL = localize('disableAll', "Disable All Installed Extensions"); - - private disposables: IDisposable[] = []; - - constructor( - id: string = DisableAllAction.ID, label: string = DisableAllAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(id, label); - this.update(); - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - } - - private update(): void { - this.enabled = this.extensionsWorkbenchService.local.some(e => e.type === LocalExtensionType.User && (e.enablementState === EnablementState.Enabled || e.enablementState === EnablementState.WorkspaceEnabled)); - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local.filter(e => e.type === LocalExtensionType.User), EnablementState.Disabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class DisableAllWorkpsaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; - static LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); - - private disposables: IDisposable[] = []; - - constructor( - id: string = DisableAllWorkpsaceAction.ID, label: string = DisableAllWorkpsaceAction.LABEL, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService - ) { - super(id, label); - this.update(); - this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); - this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); - } - - private update(): void { - this.enabled = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.extensionsWorkbenchService.local.some(e => e.type === LocalExtensionType.User && (e.enablementState === EnablementState.Enabled || e.enablementState === EnablementState.WorkspaceEnabled)); - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local.filter(e => e.type === LocalExtensionType.User), EnablementState.WorkspaceDisabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class EnableAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAll'; - static LABEL = localize('enableAll', "Enable All Extensions"); - - private disposables: IDisposable[] = []; - - constructor( - id: string = EnableAllAction.ID, label: string = EnableAllAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(id, label); - this.update(); - this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); - } - - private update(): void { - this.enabled = this.extensionsWorkbenchService.local.some(e => e.local && this.extensionEnablementService.canChangeEnablement(e.local) && (e.enablementState === EnablementState.Disabled || e.enablementState === EnablementState.WorkspaceDisabled)); - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local, EnablementState.Enabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class EnableAllWorkpsaceAction extends Action { - - static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; - static LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); - - private disposables: IDisposable[] = []; - - constructor( - id: string = EnableAllWorkpsaceAction.ID, label: string = EnableAllWorkpsaceAction.LABEL, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService - ) { - super(id, label); - this.update(); - this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); - this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); - } - - private update(): void { - this.enabled = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.extensionsWorkbenchService.local.some(e => e.local && this.extensionEnablementService.canChangeEnablement(e.local) && (e.enablementState === EnablementState.Disabled || e.enablementState === EnablementState.WorkspaceDisabled)); - } - - run(): TPromise { - return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local, EnablementState.WorkspaceEnabled); - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } -} - -export class OpenExtensionsFolderAction extends Action { - - static readonly ID = 'workbench.extensions.action.openExtensionsFolder'; - static LABEL = localize('openExtensionsFolder', "Open Extensions Folder"); - - constructor( - id: string, - label: string, - @IWindowsService private windowsService: IWindowsService, - @IFileService private fileService: IFileService, - @IEnvironmentService private environmentService: IEnvironmentService - ) { - super(id, label, null, true); - } - - run(): TPromise { - const extensionsHome = this.environmentService.extensionsPath; - - return this.fileService.resolveFile(URI.file(extensionsHome)).then(file => { - let itemToShow: string; - if (file.children && file.children.length > 0) { - itemToShow = file.children[0].resource.fsPath; - } else { - itemToShow = paths.normalize(extensionsHome, true); - } - - return this.windowsService.showItemInFolder(itemToShow); - }); - } -} - -export class InstallVSIXAction extends Action { - - static readonly ID = 'workbench.extensions.action.installVSIX'; - static LABEL = localize('installVSIX', "Install from VSIX..."); - - constructor( - id = InstallVSIXAction.ID, - label = InstallVSIXAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService private notificationService: INotificationService, - @IWindowService private windowService: IWindowService - ) { - super(id, label, 'extension-action install-vsix', true); - } - - run(): TPromise { - return this.windowService.showOpenDialog({ - title: localize('installFromVSIX', "Install from VSIX"), - filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], - properties: ['openFile'], - buttonLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) - }).then(result => { - if (!result) { - return TPromise.as(null); - } - - return TPromise.join(result.map(vsix => this.extensionsWorkbenchService.install(vsix))).then(() => { - this.notificationService.prompt( - Severity.Info, - localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), - [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => this.windowService.reloadWindow() - }] - ); - }); - }); - } -} - -export class ReinstallAction extends Action { - - static readonly ID = 'workbench.extensions.action.reinstall'; - static LABEL = localize('reinstall', "Reinstall Extension..."); - - constructor( - id: string = ReinstallAction.ID, label: string = ReinstallAction.LABEL, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IQuickOpenService private quickOpenService: IQuickOpenService, - @INotificationService private notificationService: INotificationService, - @IWindowService private windowService: IWindowService - ) { - super(id, label); - } - - get enabled(): boolean { - return this.extensionsWorkbenchService.local.filter(l => l.type === LocalExtensionType.User && l.local).length > 0; - } - - run(): TPromise { - return this.quickOpenService.pick(this.getEntries(), { placeHolder: localize('selectExtension', "Select Extension to Reinstall") }); - } - - private getEntries(): TPromise { - return this.extensionsWorkbenchService.queryLocal() - .then(local => { - const entries: IPickOpenEntry[] = local - .filter(extension => extension.type === LocalExtensionType.User) - .map(extension => { - return { - id: extension.id, - label: extension.displayName, - description: extension.id, - run: () => this.reinstallExtension(extension), - }; - }); - return entries; - }); - } - - private reinstallExtension(extension: IExtension): TPromise { - return this.extensionsWorkbenchService.reinstall(extension) - .then(() => { - this.notificationService.prompt( - Severity.Info, - localize('ReinstallAction.success', "Successfully reinstalled the extension."), - [{ - label: localize('ReinstallAction.reloadNow', "Reload Now"), - run: () => this.windowService.reloadWindow() - }] - ); - }, error => this.notificationService.error(error)); - } -} - -CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForLanguage', function (accessor: ServicesAccessor, fileExtension: string) { - const viewletService = accessor.get(IViewletService); - - return viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search(`ext:${fileExtension.replace(/^\./, '')}`); - viewlet.focus(); - }); -}); - -CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithId', function (accessor: ServicesAccessor, extensionId: string) { - const viewletService = accessor.get(IViewletService); - - return viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search(`@id:${extensionId}`); - viewlet.focus(); - }); -}); - -export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { - dark: '#327e36', - light: '#327e36', - hc: null -}, localize('extensionButtonProminentBackground', "Button background color for actions extension that stand out (e.g. install button).")); - -export const extensionButtonProminentForeground = registerColor('extensionButton.prominentForeground', { - dark: Color.white, - light: Color.white, - hc: null -}, localize('extensionButtonProminentForeground', "Button foreground color for actions extension that stand out (e.g. install button).")); - -export const extensionButtonProminentHoverBackground = registerColor('extensionButton.prominentHoverBackground', { - dark: '#28632b', - light: '#28632b', - hc: null -}, localize('extensionButtonProminentHoverBackground', "Button background hover color for actions extension that stand out (e.g. install button).")); - -registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); - } - - const buttonBackgroundColor = theme.getColor(buttonBackground); - if (buttonBackgroundColor) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action { background-color: ${buttonBackgroundColor}; }`); - } - - const buttonForegroundColor = theme.getColor(buttonForeground); - if (buttonForegroundColor) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action { color: ${buttonForegroundColor}; }`); - } - - const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground); - if (buttonHoverBackgroundColor) { - collector.addRule(`.monaco-action-bar .action-item:hover .action-label.extension-action { background-color: ${buttonHoverBackgroundColor}; }`); - } - - const contrastBorderColor = theme.getColor(contrastBorder); - if (contrastBorderColor) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); - } - - const extensionButtonProminentBackgroundColor = theme.getColor(extensionButtonProminentBackground); - if (extensionButtonProminentBackground) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); - } - - const extensionButtonProminentForegroundColor = theme.getColor(extensionButtonProminentForeground); - if (extensionButtonProminentForeground) { - collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.prominent { color: ${extensionButtonProminentForegroundColor}; }`); - } - - const extensionButtonProminentHoverBackgroundColor = theme.getColor(extensionButtonProminentHoverBackground); - if (extensionButtonProminentHoverBackground) { - collector.addRule(`.monaco-action-bar .action-item:hover .action-label.extension-action.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); - } -}); diff --git a/src/vs/workbench/parts/extensions/browser/extensionsList.ts b/src/vs/workbench/parts/extensions/browser/extensionsList.ts deleted file mode 100644 index 3cbee02d7bd..00000000000 --- a/src/vs/workbench/parts/extensions/browser/extensionsList.ts +++ /dev/null @@ -1,184 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -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'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDelegate } from 'vs/base/browser/ui/list/list'; -import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; -import { once } from 'vs/base/common/event'; -import { domEvent } from 'vs/base/browser/event'; -import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, extensionButtonProminentBackground, extensionButtonProminentForeground, MaliciousStatusLabelAction, DisabledStatusLabelAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { Label, RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; - -export interface ITemplateData { - root: HTMLElement; - element: HTMLElement; - icon: HTMLImageElement; - name: HTMLElement; - installCount: HTMLElement; - ratings: HTMLElement; - author: HTMLElement; - description: HTMLElement; - extension: IExtension; - disposables: IDisposable[]; - extensionDisposables: IDisposable[]; -} - -export class Delegate implements IDelegate { - getHeight() { return 62; } - getTemplateId() { return 'extension'; } -} - -const actionOptions = { icon: true, label: true }; - -export class Renderer implements IPagedRenderer { - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @INotificationService private notificationService: INotificationService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionService private extensionService: IExtensionService, - @IExtensionTipsService private extensionTipsService: IExtensionTipsService, - @IThemeService private themeService: IThemeService - ) { } - - 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 element = append(root, $('.extension')); - const icon = append(element, $('img.icon')); - const details = append(element, $('.details')); - const headerContainer = append(details, $('.header-container')); - const header = append(headerContainer, $('.header')); - const name = append(header, $('span.name')); - const version = append(header, $('span.version')); - const installCount = append(header, $('span.install-count')); - const ratings = append(header, $('span.ratings')); - const description = append(details, $('.description.ellipsis')); - const footer = append(details, $('.footer')); - const author = append(footer, $('.author.ellipsis')); - const actionbar = new ActionBar(footer, { - animated: false, - actionItemProvider: (action: Action) => { - if (action.id === ManageExtensionAction.ID) { - return (action).actionItem; - } - return null; - } - }); - actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); - - const versionWidget = this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version); - const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, { small: true }); - const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratings, { small: true }); - - const maliciousStatusAction = this.instantiationService.createInstance(MaliciousStatusLabelAction, false); - const disabledStatusAction = this.instantiationService.createInstance(DisabledStatusLabelAction); - const installAction = this.instantiationService.createInstance(InstallAction); - const updateAction = this.instantiationService.createInstance(UpdateAction); - const reloadAction = this.instantiationService.createInstance(ReloadAction); - const manageAction = this.instantiationService.createInstance(ManageExtensionAction); - - actionbar.push([updateAction, reloadAction, installAction, disabledStatusAction, maliciousStatusAction, manageAction], actionOptions); - const disposables = [versionWidget, installCountWidget, ratingsWidget, maliciousStatusAction, disabledStatusAction, updateAction, reloadAction, manageAction, actionbar, bookmarkStyler]; - - return { - root, element, icon, name, installCount, ratings, author, description, disposables, - extensionDisposables: [], - set extension(extension: IExtension) { - versionWidget.extension = extension; - installCountWidget.extension = extension; - ratingsWidget.extension = extension; - maliciousStatusAction.extension = extension; - disabledStatusAction.extension = extension; - installAction.extension = extension; - updateAction.extension = extension; - reloadAction.extension = extension; - manageAction.extension = extension; - } - }; - } - - renderPlaceholder(index: number, data: ITemplateData): void { - addClass(data.element, 'loading'); - - data.root.removeAttribute('aria-label'); - data.extensionDisposables = dispose(data.extensionDisposables); - data.icon.src = ''; - data.name.textContent = ''; - data.author.textContent = ''; - data.description.textContent = ''; - data.installCount.style.display = 'none'; - data.ratings.style.display = 'none'; - data.extension = null; - } - - renderElement(extension: IExtension, index: number, data: ITemplateData): void { - removeClass(data.element, 'loading'); - - data.extensionDisposables = dispose(data.extensionDisposables); - const isInstalled = this.extensionsWorkbenchService.local.some(e => e.id === extension.id); - - this.extensionService.getExtensions().then(enabledExtensions => { - const isExtensionRunning = enabledExtensions.some(e => areSameExtensions(e, extension)); - - toggleClass(data.root, 'disabled', isInstalled && !isExtensionRunning); - }); - - const onError = once(domEvent(data.icon, 'error')); - onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); - data.icon.src = extension.iconUrl; - - if (!data.icon.complete) { - data.icon.style.visibility = 'hidden'; - data.icon.onload = () => data.icon.style.visibility = 'inherit'; - } else { - data.icon.style.visibility = 'inherit'; - } - - data.root.setAttribute('aria-label', extension.displayName); - removeClass(data.root, 'recommended'); - - const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); - if (extRecommendations[extension.id.toLowerCase()]) { - data.root.setAttribute('aria-label', extension.displayName + '. ' + extRecommendations[extension.id]); - addClass(data.root, 'recommended'); - data.root.title = extRecommendations[extension.id.toLowerCase()].reasonText; - } - - data.name.textContent = extension.displayName; - data.author.textContent = extension.publisherDisplayName; - data.description.textContent = extension.description; - data.installCount.style.display = ''; - data.ratings.style.display = ''; - data.extension = extension; - } - - disposeTemplate(data: ITemplateData): void { - data.disposables = dispose(data.disposables); - } -} diff --git a/src/vs/workbench/parts/extensions/browser/extensionsQuickOpen.ts b/src/vs/workbench/parts/extensions/browser/extensionsQuickOpen.ts index 392a25bbd72..c1730158428 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsQuickOpen.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsQuickOpen.ts @@ -12,6 +12,7 @@ import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/parts/extensions/co import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CancellationToken } from 'vs/base/common/cancellation'; class SimpleEntry extends QuickOpenEntry { @@ -46,12 +47,12 @@ export class ExtensionsHandler extends QuickOpenHandler { super(); } - getResults(text: string): TPromise> { + getResults(text: string, token: CancellationToken): TPromise> { const label = nls.localize('manage', "Press Enter to manage your extensions."); const action = () => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) - .done(viewlet => { + .then(viewlet => { viewlet.search(''); viewlet.focus(); }); @@ -82,7 +83,7 @@ export class GalleryExtensionsHandler extends QuickOpenHandler { super(); } - getResults(text: string): TPromise> { + getResults(text: string, token: CancellationToken): TPromise> { if (/\./.test(text)) { return this.galleryService.query({ names: [text], pageSize: 1 }) .then(galleryResult => { @@ -100,7 +101,7 @@ export class GalleryExtensionsHandler extends QuickOpenHandler { .then(viewlet => viewlet as IExtensionsViewlet) .then(viewlet => viewlet.search(`@id:${text}`)) .then(() => this.extensionsService.installFromGallery(galleryExtension)) - .done(null, err => this.notificationService.error(err)); + .then(null, err => this.notificationService.error(err)); }; entries.push(new SimpleEntry(label, action)); @@ -117,7 +118,7 @@ export class GalleryExtensionsHandler extends QuickOpenHandler { const action = () => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) - .done(viewlet => { + .then(viewlet => { viewlet.search(text); viewlet.focus(); }); diff --git a/src/vs/workbench/parts/extensions/browser/extensionsViewer.ts b/src/vs/workbench/parts/extensions/browser/extensionsViewer.ts new file mode 100644 index 00000000000..a2aede32774 --- /dev/null +++ b/src/vs/workbench/parts/extensions/browser/extensionsViewer.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; +import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; +import { Action } from 'vs/base/common/actions'; +import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/extensions/common/extensions'; +import { once } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { WorkbenchTreeController, WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export interface IExtensionTemplateData { + icon: HTMLImageElement; + name: HTMLElement; + identifier: HTMLElement; + author: HTMLElement; + extensionDisposables: IDisposable[]; + extensionData: IExtensionData; +} + +export interface IUnknownExtensionTemplateData { + identifier: HTMLElement; +} + +export interface IExtensionData { + extension: IExtension; + hasChildren: boolean; + getChildren: () => Promise; + parent: IExtensionData; +} + +export class DataSource implements IDataSource { + + public getId(tree: ITree, { extension, parent }: IExtensionData): string { + return parent ? this.getId(tree, parent) + '/' + extension.id : extension.id; + } + + public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean { + return hasChildren; + } + + public getChildren(tree: ITree, extensionData: IExtensionData): Promise { + return extensionData.getChildren(); + } + + public getParent(tree: ITree, { parent }: IExtensionData): Promise { + return TPromise.as(parent); + } +} + +export class Renderer implements IRenderer { + + private static readonly EXTENSION_TEMPLATE_ID = 'extension-template'; + private static readonly UNKNOWN_EXTENSION_TEMPLATE_ID = 'unknown-extension-template'; + + constructor(@IInstantiationService private instantiationService: IInstantiationService) { + } + + public getHeight(tree: ITree, element: IExtensionData): number { + return 62; + } + + public getTemplateId(tree: ITree, { extension }: IExtensionData): string { + return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; + } + + public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { + if (Renderer.EXTENSION_TEMPLATE_ID === templateId) { + return this.renderExtensionTemplate(tree, container); + } + return this.renderUnknownExtensionTemplate(tree, container); + } + + private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData { + dom.addClass(container, 'extension'); + + const icon = dom.append(container, dom.$('img.icon')); + const details = dom.append(container, dom.$('.details')); + + const header = dom.append(details, dom.$('.header')); + const name = dom.append(header, dom.$('span.name')); + const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); + const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => { + tree.setFocus(openExtensionAction.extensionData); + tree.setSelection([openExtensionAction.extensionData]); + openExtensionAction.run(e.ctrlKey || e.metaKey); + e.stopPropagation(); + e.preventDefault(); + })]; + const identifier = dom.append(header, dom.$('span.identifier')); + + const footer = dom.append(details, dom.$('.footer')); + const author = dom.append(footer, dom.$('.author')); + return { + icon, + name, + identifier, + author, + extensionDisposables, + set extensionData(extensionData: IExtensionData) { + openExtensionAction.extensionData = extensionData; + } + }; + } + + private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData { + const messageContainer = dom.append(container, dom.$('div.unknown-extension')); + dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); + dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:"); + + const identifier = dom.append(messageContainer, dom.$('span.message')); + return { identifier }; + } + + public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void { + if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { + this.renderExtension(tree, element, templateData); + return; + } + this.renderUnknownExtension(tree, element, templateData); + } + + private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void { + const extension = extensionData.extension; + const onError = once(domEvent(data.icon, 'error')); + onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); + data.icon.src = extension.iconUrl; + + if (!data.icon.complete) { + data.icon.style.visibility = 'hidden'; + data.icon.onload = () => data.icon.style.visibility = 'inherit'; + } else { + data.icon.style.visibility = 'inherit'; + } + + data.name.textContent = extension.displayName; + data.identifier.textContent = extension.id; + data.author.textContent = extension.publisherDisplayName; + data.extensionData = extensionData; + } + + private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void { + data.identifier.textContent = extension.id; + } + + public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { + if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { + templateData.extensionDisposables = dispose((templateData).extensionDisposables); + } + } +} + +export class Controller extends WorkbenchTreeController { + + constructor( + @IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService, + @IConfigurationService configurationService: IConfigurationService + ) { + super({}, configurationService); + + // TODO@Sandeep this should be a command + this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); + } + + protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean { + let currentFocused = tree.getFocus(); + if (super.onLeftClick(tree, element, event)) { + if (element.parent === null) { + if (currentFocused) { + tree.setFocus(currentFocused); + } else { + tree.focusFirst(); + } + return true; + } + } + return false; + } + + public openExtension(tree: ITree, sideByside: boolean): boolean { + const element: IExtensionData = tree.getFocus(); + if (element.extension) { + this.extensionsWorkdbenchService.open(element.extension, sideByside); + return true; + } + return false; + } +} + +class OpenExtensionAction extends Action { + + private _extensionData: IExtensionData; + + constructor(@IExtensionsWorkbenchService private extensionsWorkdbenchService: IExtensionsWorkbenchService) { + super('extensions.action.openExtension', ''); + } + + public set extensionData(extension: IExtensionData) { + this._extensionData = extension; + } + + public get extensionData(): IExtensionData { + return this._extensionData; + } + + run(sideByside: boolean): TPromise { + return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside); + } +} + +export class ExtensionsTree extends WorkbenchTree { + + constructor( + input: IExtensionData, + container: HTMLElement, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + const renderer = instantiationService.createInstance(Renderer); + const controller = instantiationService.createInstance(Controller); + + super( + container, + { + dataSource: new DataSource(), + renderer, + controller + }, { + indentPixels: 40, + twistiePixels: 20 + }, + contextKeyService, listService, themeService, instantiationService, configurationService + ); + + this.setInput(input); + + this.disposables.push(this.onDidChangeSelection(event => { + if (event && event.payload && event.payload.origin === 'keyboard') { + controller.openExtension(this, false); + } + })); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/media/language-icon.png b/src/vs/workbench/parts/extensions/browser/media/language-icon.png deleted file mode 100644 index 7f7e56c098e..00000000000 Binary files a/src/vs/workbench/parts/extensions/browser/media/language-icon.png and /dev/null differ diff --git a/src/vs/workbench/parts/extensions/browser/media/loading.svg b/src/vs/workbench/parts/extensions/browser/media/loading.svg deleted file mode 100644 index 0098fd9f264..00000000000 --- a/src/vs/workbench/parts/extensions/browser/media/loading.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/common/extensionQuery.ts b/src/vs/workbench/parts/extensions/common/extensionQuery.ts index 5dd7153d2ae..5f85a0ee0e0 100644 --- a/src/vs/workbench/parts/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/parts/extensions/common/extensionQuery.ts @@ -3,22 +3,59 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { flatten } from 'vs/base/common/arrays'; + export class Query { - constructor(public value: string, public sortBy: string) { + constructor(public value: string, public sortBy: string, public groupBy: string) { this.value = value.trim(); } + static suggestions(query: string): string[] { + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext']; + const subcommands = { + 'sort': ['installs', 'rating', 'name'], + 'category': ['"programming languages"', 'snippets', 'linters', 'themes', 'debuggers', 'formatters', 'keymaps', '"scm providers"', 'other', '"extension packs"', '"language packs"'], + 'tag': [''], + 'ext': [''] + }; + + let queryContains = (substr: string) => query.indexOf(substr) > -1; + let hasSort = subcommands.sort.some(subcommand => queryContains(`@sort:${subcommand}`)); + let hasCategory = subcommands.category.some(subcommand => queryContains(`@category:${subcommand}`)); + + return flatten( + commands.map(command => { + if (hasSort && command === 'sort' || hasCategory && command === 'category') { + return []; + } + if (subcommands[command]) { + return subcommands[command].map(subcommand => `@${command}:${subcommand}${subcommand === '' ? '' : ' '}`); + } + else { + return [`@${command} `]; + } + })); + + } + static parse(value: string): Query { let sortBy = ''; - value = value.replace(/@sort:(\w+)(-\w*)?/g, (match, by: string, order: string) => { sortBy = by; return ''; }); - return new Query(value, sortBy); + let groupBy = ''; + value = value.replace(/@group:(\w+)(-\w*)?/g, (match, by: string, order: string) => { + groupBy = by; + + return ''; + }); + + return new Query(value, sortBy, groupBy); } toString(): string { @@ -27,6 +64,9 @@ export class Query { if (this.sortBy) { result = `${result}${result ? ' ' : ''}@sort:${this.sortBy}`; } + if (this.groupBy) { + result = `${result}${result ? ' ' : ''}@group:${this.groupBy}`; + } return result; } diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index 0a8636cb1ef..ca6f8e8204b 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -8,9 +8,10 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IPager } from 'vs/base/common/paging'; -import { IQueryOptions, IExtensionManifest, LocalExtensionType, EnablementState, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IQueryOptions, IExtensionManifest, LocalExtensionType, EnablementState, ILocalExtension, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const VIEWLET_ID = 'workbench.view.extensions'; export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); @@ -19,7 +20,7 @@ export interface IExtensionsViewlet extends IViewlet { search(text: string): void; } -export enum ExtensionState { +export const enum ExtensionState { Installing, Installed, Uninstalling, @@ -39,7 +40,6 @@ export interface IExtension { latestVersion: string; description: string; url: string; - downloadUrl: string; repository: string; iconUrl: string; iconUrlFallback: string; @@ -50,12 +50,17 @@ export interface IExtension { outdated: boolean; enablementState: EnablementState; dependencies: string[]; + extensionPack: string[]; telemetryData: any; preview: boolean; - getManifest(): TPromise; - getReadme(): TPromise; - getChangelog(): TPromise; + getManifest(token: CancellationToken): TPromise; + getReadme(token: CancellationToken): TPromise; + hasReadme(): boolean; + getChangelog(token: CancellationToken): TPromise; + hasChangelog(): boolean; local?: ILocalExtension; + locals?: ILocalExtension[]; + gallery?: IGalleryExtension; isMalicious: boolean; } @@ -73,7 +78,7 @@ export const IExtensionsWorkbenchService = createDecorator; + onChange: Event; local: IExtension[]; queryLocal(): TPromise; queryGallery(options?: IQueryOptions): TPromise>; @@ -83,7 +88,7 @@ export interface IExtensionsWorkbenchService { uninstall(extension: IExtension): TPromise; reinstall(extension: IExtension): TPromise; setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): TPromise; - loadDependencies(extension: IExtension): TPromise; + loadDependencies(extension: IExtension, token: CancellationToken): TPromise; open(extension: IExtension, sideByside?: boolean): TPromise; checkForUpdates(): TPromise; allowedBadgeProviders: string[]; @@ -91,10 +96,14 @@ export interface IExtensionsWorkbenchService { export const ConfigurationKey = 'extensions'; export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; +export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates'; export const ShowRecommendationsOnlyOnDemandKey = 'extensions.showRecommendationsOnlyOnDemand'; +export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange'; export interface IExtensionsConfiguration { autoUpdate: boolean; + autoCheckUpdates: boolean; ignoreRecommendations: boolean; showRecommendationsOnlyOnDemand: boolean; + closeExtensionDetailsOnViewChange: boolean; } diff --git a/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts b/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts index fc81cb063d1..7f719e46e5f 100644 --- a/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts +++ b/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts @@ -13,10 +13,20 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { allowComments: true, type: 'object', title: localize('app.extensions.json.title', "Extensions"), + additionalProperties: false, properties: { recommendations: { type: 'array', - description: localize('app.extensions.json.recommendations', "List of extensions recommendations. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), + description: localize('app.extensions.json.recommendations', "List of extensions which should be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") + }, + }, + unwantedRecommendations: { + type: 'array', + description: localize('app.extensions.json.unwantedRecommendations', "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), items: { type: 'string', pattern: EXTENSION_IDENTIFIER_PATTERN, @@ -28,10 +38,15 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { export const ExtensionsConfigurationInitialContent: string = [ '{', - '\t// See http://go.microsoft.com/fwlink/?LinkId=827846', - '\t// for the documentation about the extensions.json format', + '\t// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.', + '\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp', + '', + '\t// List of extensions which should be recommended for users of this workspace.', '\t"recommendations": [', - '\t\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp', + '\t\t', + '\t],', + '\t// List of extensions recommended by VS Code that should not be recommended for users of this workspace.', + '\t"unwantedRecommendations": [', '\t\t', '\t]', '}' diff --git a/src/vs/workbench/parts/extensions/common/extensionsInput.ts b/src/vs/workbench/parts/extensions/common/extensionsInput.ts index 02f28094501..73557933c4b 100644 --- a/src/vs/workbench/parts/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/parts/extensions/common/extensionsInput.ts @@ -9,14 +9,16 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput } from 'vs/workbench/common/editor'; import { IExtension } from 'vs/workbench/parts/extensions/common/extensions'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export class ExtensionsInput extends EditorInput { static readonly ID = 'workbench.extensions.input2'; get extension(): IExtension { return this._extension; } - constructor(private _extension: IExtension) { + constructor( + private _extension: IExtension, + ) { super(); } @@ -39,7 +41,7 @@ export class ExtensionsInput extends EditorInput { return this.extension === otherExtensionInput.extension; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts index f6120fd51f7..47220facb67 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -7,13 +7,13 @@ import 'vs/css!./media/extensionEditor'; import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { marked } from 'vs/base/common/marked/marked'; -import { always } from 'vs/base/common/async'; +import { createCancelablePromise } from 'vs/base/common/async'; import * as arrays from 'vs/base/common/arrays'; import { OS } from 'vs/base/common/platform'; import { Event, Emitter, once, chain } from 'vs/base/common/event'; -import Cache from 'vs/base/common/cache'; +import { Cache, CacheResult } from 'vs/base/common/cache'; import { Action } from 'vs/base/common/actions'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; @@ -27,13 +27,11 @@ import { IExtensionManifest, IKeyBinding, IView, IExtensionTipsService, LocalExt 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 } from 'vs/workbench/parts/extensions/common/extensions'; -import { Renderer, DataSource, Controller } from 'vs/workbench/parts/extensions/browser/dependenciesViewer'; import { RatingsWidget, InstallCountWidget } from 'vs/workbench/parts/extensions/browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, EnableAction, DisableAction, ReloadAction, MaliciousStatusLabelAction, DisabledStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { WebviewElement } from 'vs/workbench/parts/webview/electron-browser/webviewElement'; -import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -41,20 +39,17 @@ import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Command, ICommandOptions } from 'vs/editor/browser/editorExtensions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Command } from 'vs/editor/browser/editorExtensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Color } from 'vs/base/common/color'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { assign } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; - -/** A context key that is set when an extension editor webview has focus. */ -export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey('extensionEditorWebviewFocus', undefined); -/** A context key that is set when the find widget find input in extension editor webview is focused. */ -export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey('extensionEditorFindWidgetInputFocused', false); +import { ExtensionsTree, IExtensionData } from 'vs/workbench/parts/extensions/browser/extensionsViewer'; +import { ShowCurrentReleaseNotesAction } from 'vs/workbench/parts/update/electron-browser/update'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; function renderBody(body: string): string { const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://', 'vscode-core-resource://'); @@ -138,7 +133,8 @@ const NavbarSection = { Readme: 'readme', Contributions: 'contributions', Changelog: 'changelog', - Dependencies: 'dependencies' + Dependencies: 'dependencies', + ExtensionPack: 'extensionPack' }; interface ILayoutParticipant { @@ -164,6 +160,8 @@ export class ExtensionEditor extends BaseEditor { private navbar: NavBar; private content: HTMLElement; private recommendation: HTMLElement; + private recommendationText: HTMLElement; + private ignoreActionbar: ActionBar; private header: HTMLElement; private extensionReadme: Cache; @@ -171,8 +169,6 @@ export class ExtensionEditor extends BaseEditor { private extensionManifest: Cache; private extensionDependencies: Cache; - private contextKey: IContextKey; - private findInputFocusContextKey: IContextKey; private layoutParticipants: ILayoutParticipant[] = []; private contentDisposables: IDisposable[] = []; private transientDisposables: IDisposable[] = []; @@ -190,7 +186,6 @@ export class ExtensionEditor extends BaseEditor { @INotificationService private readonly notificationService: INotificationService, @IOpenerService private readonly openerService: IOpenerService, @IPartService private readonly partService: IPartService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, ) { super(ExtensionEditor.ID, telemetryService, themeService); @@ -199,8 +194,6 @@ export class ExtensionEditor extends BaseEditor { this.extensionChangelog = null; this.extensionManifest = null; this.extensionDependencies = null; - this.contextKey = KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS.bindTo(this.contextKeyService); - this.findInputFocusContextKey = KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this.contextKeyService); } createEditor(parent: HTMLElement): void { @@ -250,15 +243,24 @@ export class ExtensionEditor extends BaseEditor { return null; } }); - this.disposables.push(this.extensionActionBar); this.recommendation = append(details, $('.recommendation')); + this.recommendationText = append(this.recommendation, $('.recommendation-text')); + this.ignoreActionbar = new ActionBar(this.recommendation, { animated: false }); + + this.disposables.push(this.extensionActionBar); + this.disposables.push(this.ignoreActionbar); chain(this.extensionActionBar.onDidRun) .map(({ error }) => error) .filter(error => !!error) .on(this.onError, this, this.disposables); + chain(this.ignoreActionbar.onDidRun) + .map(({ error }) => error) + .filter(error => !!error) + .on(this.onError, this, this.disposables); + const body = append(root, $('.body')); this.navbar = new NavBar(body); @@ -271,10 +273,10 @@ export class ExtensionEditor extends BaseEditor { this.transientDisposables = dispose(this.transientDisposables); - this.extensionReadme = new Cache(() => extension.getReadme()); - this.extensionChangelog = new Cache(() => extension.getChangelog()); - this.extensionManifest = new Cache(() => extension.getManifest()); - this.extensionDependencies = new Cache(() => this.extensionsWorkbenchService.loadDependencies(extension)); + this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token))); + this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token))); + this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token))); + this.extensionDependencies = new Cache(() => createCancelablePromise(token => this.extensionsWorkbenchService.loadDependencies(extension, token))); const onError = once(domEvent(this.icon, 'error')); onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables); @@ -288,24 +290,30 @@ export class ExtensionEditor extends BaseEditor { this.publisher.textContent = extension.publisherDisplayName; this.description.textContent = extension.description; + removeClass(this.header, 'recommendation-ignored'); + removeClass(this.header, 'recommended'); + const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); let recommendationsData = {}; if (extRecommendations[extension.id.toLowerCase()]) { addClass(this.header, 'recommended'); - this.recommendation.textContent = extRecommendations[extension.id.toLowerCase()].reasonText; + this.recommendationText.textContent = extRecommendations[extension.id.toLowerCase()].reasonText; recommendationsData = { recommendationReason: extRecommendations[extension.id.toLowerCase()].reasonId }; - } else { - removeClass(this.header, 'recommended'); - this.recommendation.textContent = ''; + } else if (this.extensionTipsService.getAllIgnoredRecommendations().global.indexOf(extension.id.toLowerCase()) !== -1) { + addClass(this.header, 'recommendation-ignored'); + this.recommendationText.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + } + else { + this.recommendationText.textContent = ''; } /* __GDPR__ - "extensionGallery:openExtension" : { - "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } + "extensionGallery:openExtension" : { + "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } */ this.telemetryService.publicLog('extensionGallery:openExtension', assign(extension.telemetryData, recommendationsData)); @@ -318,7 +326,7 @@ export class ExtensionEditor extends BaseEditor { this.publisher.onclick = finalHandler(() => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) - .done(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); + .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); }); if (extension.licenseUrl) { @@ -332,6 +340,8 @@ export class ExtensionEditor extends BaseEditor { this.name.onclick = null; this.rating.onclick = null; this.publisher.onclick = null; + this.license.onclick = null; + this.license.style.display = 'none'; } if (extension.repository) { @@ -369,16 +379,58 @@ export class ExtensionEditor extends BaseEditor { this.extensionActionBar.push([disabledStatusAction, reloadAction, updateAction, enableAction, disableAction, installAction, maliciousStatusAction], { icon: true, label: true }); this.transientDisposables.push(enableAction, updateAction, reloadAction, disableAction, installAction, maliciousStatusAction, disabledStatusAction); + const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction); + const undoIgnoreAction = this.instantiationService.createInstance(UndoIgnoreExtensionRecommendationAction); + ignoreAction.extension = extension; + undoIgnoreAction.extension = extension; + + this.extensionTipsService.onRecommendationChange(change => { + if (change.extensionId.toLowerCase() === extension.id.toLowerCase()) { + if (change.isRecommended) { + removeClass(this.header, 'recommendation-ignored'); + const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); + if (extRecommendations[extension.id.toLowerCase()]) { + addClass(this.header, 'recommended'); + this.recommendationText.textContent = extRecommendations[extension.id.toLowerCase()].reasonText; + } + } else { + addClass(this.header, 'recommendation-ignored'); + removeClass(this.header, 'recommended'); + this.recommendationText.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + } + } + }); + + this.ignoreActionbar.clear(); + this.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); + this.transientDisposables.push(ignoreAction, undoIgnoreAction); + this.content.innerHTML = ''; // Clear content before setting navbar actions. this.navbar.clear(); this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables); - this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); - this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); - this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); - this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); - this.editorLoadComplete = true; + if (extension.hasReadme()) { + this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + } + this.extensionManifest.get() + .promise + .then(manifest => { + if (extension.extensionPack.length) { + this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); + } + if (manifest && manifest.contributes) { + this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); + } + if (extension.hasChangelog()) { + this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); + } + if (extension.dependencies.length) { + this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); + } + this.editorLoadComplete = true; + }); + return super.setInput(input, options, token); } @@ -388,18 +440,6 @@ export class ExtensionEditor extends BaseEditor { } } - public showNextFindTerm() { - if (this.activeWebview) { - this.activeWebview.showNextFindTerm(); - } - } - - public showPreviousFindTerm() { - if (this.activeWebview) { - this.activeWebview.showPreviousFindTerm(); - } - } - private onNavbarChange(extension: IExtension, id: string): void { if (this.editorLoadComplete) { /* __GDPR__ @@ -421,49 +461,54 @@ export class ExtensionEditor extends BaseEditor { case NavbarSection.Contributions: return this.openContributions(); case NavbarSection.Changelog: return this.openChangelog(); case NavbarSection.Dependencies: return this.openDependencies(extension); + case NavbarSection.ExtensionPack: return this.openExtensionPack(extension); } } - private openMarkdown(content: TPromise, noContentCopy: string) { - return this.loadContents(() => content + private openMarkdown(cacheResult: CacheResult, noContentCopy: string): void { + this.loadContents(() => cacheResult) .then(marked.parse) .then(renderBody) .then(removeEmbeddedSVGs) .then(body => { const allowedBadgeProviders = this.extensionsWorkbenchService.allowedBadgeProviders; const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : {}; - this.activeWebview = this.instantiationService.createInstance(WebviewElement, this.partService.getContainer(Parts.EDITOR_PART), this.contextKey, this.findInputFocusContextKey, webViewOptions); + this.activeWebview = this.instantiationService.createInstance(WebviewElement, this.partService.getContainer(Parts.EDITOR_PART), webViewOptions); this.activeWebview.mountTo(this.content); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, this.activeWebview); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); this.activeWebview.contents = body; this.activeWebview.onDidClickLink(link => { + if (!link) { + return; + } // Whitelist supported schemes for links - if (link && ['http', 'https', 'mailto'].indexOf(link.scheme) >= 0) { + if (['http', 'https', 'mailto'].indexOf(link.scheme) >= 0 || (link.scheme === 'command' && link.path === ShowCurrentReleaseNotesAction.ID)) { this.openerService.open(link); } }, null, this.contentDisposables); this.contentDisposables.push(this.activeWebview); + this.activeWebview.focus(); }) .then(null, () => { const p = append(this.content, $('p.nocontent')); p.textContent = noContentCopy; - })); + }); } - private openReadme() { - return this.openMarkdown(this.extensionReadme.get(), localize('noReadme', "No README available.")); + private openReadme(): void { + this.openMarkdown(this.extensionReadme.get(), localize('noReadme', "No README available.")); } - private openChangelog() { - return this.openMarkdown(this.extensionChangelog.get(), localize('noChangelog', "No Changelog available.")); + private openChangelog(): void { + this.openMarkdown(this.extensionChangelog.get(), localize('noChangelog', "No Changelog available.")); } - private openContributions() { - return this.loadContents(() => this.extensionManifest.get() + private openContributions(): void { + const content = $('div', { class: 'subcontent', tabindex: '0' }); + this.loadContents(() => this.extensionManifest.get()) .then(manifest => { - const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); const layout = () => scrollableContent.scanDomNode(); @@ -488,75 +533,140 @@ export class ExtensionEditor extends BaseEditor { scrollableContent.scanDomNode(); if (isEmpty) { - append(this.content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - return; + append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); + append(this.content, content); } else { append(this.content, scrollableContent.getDomNode()); this.contentDisposables.push(scrollableContent); } + content.focus(); }, () => { - append(this.content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - })); + append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); + append(this.content, content); + content.focus(); + }); } - private openDependencies(extension: IExtension) { + private openDependencies(extension: IExtension): void { if (extension.dependencies.length === 0) { append(this.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); return; } - return this.loadContents(() => { - return this.extensionDependencies.get().then(extensionDependencies => { + this.loadContents(() => this.extensionDependencies.get()) + .then(extensionDependencies => { const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); append(this.content, scrollableContent.getDomNode()); this.contentDisposables.push(scrollableContent); - const tree = this.renderDependencies(content, extensionDependencies); + const dependenciesTree = this.renderDependencies(content, extensionDependencies); const layout = () => { scrollableContent.scanDomNode(); const scrollDimensions = scrollableContent.getScrollDimensions(); - tree.layout(scrollDimensions.height); + dependenciesTree.layout(scrollDimensions.height); }; const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); - this.contentDisposables.push(tree); + this.contentDisposables.push(dependenciesTree); scrollableContent.scanDomNode(); + dependenciesTree.domFocus(); }, error => { append(this.content, $('p.nocontent')).textContent = error; this.notificationService.error(error); }); - }); } private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): Tree { - const renderer = this.instantiationService.createInstance(Renderer); - const controller = this.instantiationService.createInstance(Controller); - const tree = this.instantiationService.createInstance(WorkbenchTree, container, { - dataSource: new DataSource(), - renderer, - controller - }, { - indentPixels: 40, - twistiePixels: 20 - }); + class ExtensionData implements IExtensionData { - tree.setInput(extensionDependencies); + private readonly extensionDependencies: IExtensionDependencies; - this.contentDisposables.push(tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - controller.openExtension(tree, false); + constructor(extensionDependencies: IExtensionDependencies) { + this.extensionDependencies = extensionDependencies; } - })); - return tree; + get extension(): IExtension { + return this.extensionDependencies.extension; + } + + get parent(): IExtensionData { + return this.extensionDependencies.dependent ? new ExtensionData(this.extensionDependencies.dependent) : null; + } + + get hasChildren(): boolean { + return this.extensionDependencies.hasDependencies; + } + + getChildren(): Promise { + return this.extensionDependencies.dependencies ? TPromise.as(this.extensionDependencies.dependencies.map(d => new ExtensionData(d))) : null; + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extensionDependencies), container); + } + + private openExtensionPack(extension: IExtension): void { + const content = $('div', { class: 'subcontent' }); + const scrollableContent = new DomScrollableElement(content, {}); + append(this.content, scrollableContent.getDomNode()); + this.contentDisposables.push(scrollableContent); + + const extensionsPackTree = this.renderExtensionPack(content, extension); + const layout = () => { + scrollableContent.scanDomNode(); + const scrollDimensions = scrollableContent.getScrollDimensions(); + extensionsPackTree.layout(scrollDimensions.height); + }; + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + this.contentDisposables.push(toDisposable(removeLayoutParticipant)); + + this.contentDisposables.push(extensionsPackTree); + scrollableContent.scanDomNode(); + extensionsPackTree.domFocus(); + } + + private renderExtensionPack(container: HTMLElement, extension: IExtension): Tree { + const extensionsWorkbenchService = this.extensionsWorkbenchService; + class ExtensionData implements IExtensionData { + + readonly extension: IExtension; + readonly parent: IExtensionData; + + constructor(extension: IExtension, parent?: IExtensionData) { + this.extension = extension; + this.parent = parent; + } + + get hasChildren(): boolean { + return this.extension.extensionPack.length > 0; + } + + getChildren(): Promise { + if (this.hasChildren) { + const names = arrays.distinct(this.extension.extensionPack, e => e.toLowerCase()); + return extensionsWorkbenchService.queryGallery({ names, pageSize: names.length }) + .then(result => result.firstPage.map(extension => new ExtensionData(extension, this))); + } + return TPromise.as(null); + } + } + + return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension), container); } private renderSettings(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contributes = manifest.contributes; const configuration = contributes && contributes.configuration; - const properties = configuration && configuration.properties; + let properties = {}; + if (Array.isArray(configuration)) { + configuration.forEach(config => { + properties = { ...properties, ...config.properties }; + }); + } else if (configuration) { + properties = configuration.properties; + } const contrib = properties ? Object.keys(properties) : []; if (!contrib.length) { @@ -583,8 +693,6 @@ export class ExtensionEditor extends BaseEditor { return true; } - - private renderDebuggers(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contributes = manifest.contributes; const contrib = contributes && contributes.debuggers || []; @@ -813,7 +921,7 @@ export class ExtensionEditor extends BaseEditor { }); }); - const rawKeybindings = contributes && contributes.keybindings || []; + const rawKeybindings = contributes && contributes.keybindings ? (Array.isArray(contributes.keybindings) ? contributes.keybindings : [contributes.keybindings]) : []; rawKeybindings.forEach(rawKeybinding => { const keybinding = this.resolveKeybinding(rawKeybinding); @@ -943,7 +1051,7 @@ export class ExtensionEditor extends BaseEditor { case 'darwin': key = rawKeyBinding.mac; break; } - const keyBinding = KeybindingIO.readKeybinding(key || rawKeyBinding.key, OS); + const keyBinding = KeybindingParser.parseKeybinding(key || rawKeyBinding.key, OS); if (!keyBinding) { return null; } @@ -951,13 +1059,16 @@ export class ExtensionEditor extends BaseEditor { return this.keybindingService.resolveKeybinding(keyBinding)[0]; } - private loadContents(loadingTask: () => TPromise): void { + private loadContents(loadingTask: () => CacheResult): Thenable { addClass(this.content, 'loading'); - let promise = loadingTask(); - promise = always(promise, () => removeClass(this.content, 'loading')); + const result = loadingTask(); + const onDone = () => removeClass(this.content, 'loading'); + result.promise.then(onDone, onDone); - this.contentDisposables.push(toDisposable(() => promise.cancel())); + this.contentDisposables.push(toDisposable(() => result.dispose())); + + return result.promise; } layout(): void { @@ -997,52 +1108,10 @@ class ShowExtensionEditorFindCommand extends Command { } const showCommand = new ShowExtensionEditorFindCommand({ id: 'editor.action.extensioneditor.showfind', - precondition: KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS, + precondition: ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F + primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + weight: KeybindingWeight.EditorContrib } }); -KeybindingsRegistry.registerCommandAndKeybindingRule(showCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class ShowExtensionEditorFindTermCommand extends Command { - constructor(opts: ICommandOptions, private _next: boolean) { - super(opts); - } - - public runCommand(accessor: ServicesAccessor, args: any): void { - const extensionEditor = this.getExtensionEditor(accessor); - if (extensionEditor) { - if (this._next) { - extensionEditor.showNextFindTerm(); - } else { - extensionEditor.showPreviousFindTerm(); - } - } - } - - private getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor { - const activeControl = accessor.get(IEditorService).activeControl as ExtensionEditor; - if (activeControl instanceof ExtensionEditor) { - return activeControl; - } - return null; - } -} - -const showNextFindTermCommand = new ShowExtensionEditorFindTermCommand({ - id: 'editor.action.extensioneditor.showNextFindTerm', - precondition: KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.DownArrow - } -}, true); -KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -const showPreviousFindTermCommand = new ShowExtensionEditorFindTermCommand({ - id: 'editor.action.extensioneditor.showPreviousFindTerm', - precondition: KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.UpArrow - } -}, false); -KeybindingsRegistry.registerCommandAndKeybindingRule(showPreviousFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); +showCommand.register(); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionProfileService.ts index 7ecf2cae755..d16d30ed73d 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionProfileService.ts @@ -12,7 +12,8 @@ import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/wor import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { append, $, addDisposableListener } from 'vs/base/browser/dom'; -import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionHostProfileService, ProfileSessionState, RuntimeExtensionsInput } from 'vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index aaf3489e160..89f50e43e36 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -10,23 +10,25 @@ import { forEach } from 'vs/base/common/collections'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { match } from 'vs/base/common/glob'; import * as json from 'vs/base/common/json'; -import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, ExtensionRecommendationReason, LocalExtensionType, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { + IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, ExtensionRecommendationReason, LocalExtensionType, EXTENSION_IDENTIFIER_PATTERN, + IExtensionsConfigContent, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource, InstallOperation +} from 'vs/platform/extensionManagement/common/extensionManagement'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModel } from 'vs/editor/common/model'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import product from 'vs/platform/node/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; +import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import Severity from 'vs/base/common/severity'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionsConfiguration, ConfigurationKey, ShowRecommendationsOnlyOnDemandKey, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsConfiguration, ConfigurationKey, ShowRecommendationsOnlyOnDemandKey, IExtensionsViewlet, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as pfs from 'vs/base/node/pfs'; import * as os from 'os'; -import { flatten, distinct, shuffle } from 'vs/base/common/arrays'; +import { flatten, distinct, shuffle, coalesce } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { guessMimeTypes, MIME_UNKNOWN } from 'vs/base/common/mime'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -36,12 +38,13 @@ import { asJson } from 'vs/base/node/request'; import { isNumber } from 'vs/base/common/types'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Emitter, Event } from 'vs/base/common/event'; +import { assign } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; +import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { CancellationToken } from 'vs/base/common/cancellation'; -interface IExtensionsContent { - recommendations: string[]; -} - -const empty: { [key: string]: any; } = Object.create(null); const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); const searchMarketplace = localize('searchMarketplace', "Search Marketplace"); @@ -52,20 +55,40 @@ interface IDynamicWorkspaceRecommendations { recommendations: string[]; } +function caseInsensitiveGet(obj: { [key: string]: T }, key: string): T | undefined { + if (!obj) { + return undefined; + } + for (const _key in obj) { + if (Object.hasOwnProperty.call(obj, _key) && _key.toLowerCase() === key.toLowerCase()) { + return obj[_key]; + } + } + return undefined; +} + export class ExtensionTipsService extends Disposable implements IExtensionTipsService { _serviceBrand: any; - private _fileBasedRecommendations: { [id: string]: number; } = Object.create(null); + private _fileBasedRecommendations: { [id: string]: { recommendedTime: number, sources: ExtensionRecommendationSource[] }; } = Object.create(null); private _exeBasedRecommendations: { [id: string]: string; } = Object.create(null); private _availableRecommendations: { [pattern: string]: string[] } = Object.create(null); - private _allWorkspaceRecommendedExtensions: string[] = []; + private _allWorkspaceRecommendedExtensions: IExtensionRecommendation[] = []; private _dynamicWorkspaceRecommendations: string[] = []; + private _experimentalRecommendations: { [id: string]: string } = Object.create(null); + private _allIgnoredRecommendations: string[] = []; + private _globallyIgnoredRecommendations: string[] = []; + private _workspaceIgnoredRecommendations: string[] = []; private _extensionsRecommendationsUrl: string; private _disposables: IDisposable[] = []; - public promptWorkspaceRecommendationsPromise: TPromise; + public loadWorkspaceConfigPromise: TPromise; private proactiveRecommendationsFetched: boolean = false; + private readonly _onRecommendationChange: Emitter = new Emitter(); + onRecommendationChange: Event = this._onRecommendationChange.event; + private sessionSeed: number; + constructor( @IExtensionGalleryService private readonly _galleryService: IExtensionGalleryService, @IModelService private readonly _modelService: IModelService, @@ -80,7 +103,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe @IExtensionService private extensionService: IExtensionService, @IRequestService private requestService: IRequestService, @IViewletService private viewletService: IViewletService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + @IExtensionManagementService private extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService private extensionWorkbenchService: IExtensionsWorkbenchService, + @IExperimentService private experimentService: IExperimentService, ) { super(); @@ -92,20 +118,47 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; } - this.getCachedDynamicWorkspaceRecommendations(); - this._suggestFileBasedRecommendations(); - this.promptWorkspaceRecommendationsPromise = this._suggestWorkspaceRecommendations(); + this.sessionSeed = +new Date(); + let globallyIgnored = JSON.parse(this.storageService.get('extensionsAssistant/ignored_recommendations', StorageScope.GLOBAL, '[]')); + this._globallyIgnoredRecommendations = globallyIgnored.map(id => id.toLowerCase()); + + this.fetchCachedDynamicWorkspaceRecommendations(); + this.fetchFileBasedRecommendations(); + this.fetchExperimentalRecommendations(); if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { this.fetchProactiveRecommendations(true); } + this.loadWorkspaceConfigPromise = this.getWorkspaceRecommendations().then(() => { + this.promptWorkspaceRecommendations(); + this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); + this._modelService.getModels().forEach(model => this.promptFiletypeBasedRecommendations(model)); + }); + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (!this.proactiveRecommendationsFetched && !this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { this.fetchProactiveRecommendations(); } })); + this._register(this.extensionManagementService.onDidInstallExtension(e => { + if (e.gallery && e.operation === InstallOperation.Install) { + const extRecommendations = this.getAllRecommendationsWithReason() || {}; + const recommendationReason = extRecommendations[e.gallery.identifier.id.toLowerCase()]; + if (recommendationReason) { + /* __GDPR__ + "extensionGallery:install:recommendations" : { + "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('extensionGallery:install:recommendations', assign(e.gallery.telemetryData, { recommendationReason: recommendationReason.reasonId })); + } + } + })); } private fetchProactiveRecommendations(calledDuringStartup?: boolean): TPromise { @@ -118,7 +171,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe fetchPromise = new TPromise((c, e) => { setTimeout(() => { - TPromise.join([this._suggestBasedOnExecutables(), this.getDynamicWorkspaceRecommendations()]).then(() => c(null)); + TPromise.join([this.fetchExecutableRecommendations(), this.fetchDynamicWorkspaceRecommendations()]).then(() => c(null)); }, calledDuringStartup ? 10000 : 0); }); @@ -127,7 +180,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } private isEnabled(): boolean { - return this._galleryService.isEnabled() && !this.environmentService.extensionDevelopmentPath; + return this._galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI; } getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } { @@ -137,9 +190,15 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return output; } + forEach(this._experimentalRecommendations, entry => output[entry.key.toLowerCase()] = { + reasonId: ExtensionRecommendationReason.Experimental, + reasonText: entry.value + }); + if (this.contextService.getWorkspace().folders && this.contextService.getWorkspace().folders.length === 1) { const currentRepo = this.contextService.getWorkspace().folders[0].name; - this._dynamicWorkspaceRecommendations.forEach(x => output[x.toLowerCase()] = { + + this._dynamicWorkspaceRecommendations.forEach(id => output[id.toLowerCase()] = { reasonId: ExtensionRecommendationReason.DynamicWorkspace, reasonText: localize('dynamicWorkspaceRecommendation', "This extension may interest you because it's popular among users of the {0} repository.", currentRepo) }); @@ -150,142 +209,232 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value) }); - Object.keys(this._fileBasedRecommendations).forEach(x => output[x.toLowerCase()] = { + forEach(this._fileBasedRecommendations, entry => output[entry.key.toLowerCase()] = { reasonId: ExtensionRecommendationReason.File, reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.") }); - this._allWorkspaceRecommendedExtensions.forEach(x => output[x.toLowerCase()] = { + this._allWorkspaceRecommendedExtensions.forEach(({ extensionId }) => output[extensionId.toLowerCase()] = { reasonId: ExtensionRecommendationReason.Workspace, reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.") }); + for (const id of this._allIgnoredRecommendations) { + delete output[id]; + } + return output; } - getWorkspaceRecommendations(): TPromise { - if (!this.isEnabled()) { - return TPromise.as([]); - } + getAllIgnoredRecommendations(): { global: string[], workspace: string[] } { + return { + global: this._globallyIgnoredRecommendations, + workspace: this._workspaceIgnoredRecommendations + }; + } + + getWorkspaceRecommendations(): TPromise { + if (!this.isEnabled()) { return TPromise.as([]); } + return this.fetchWorkspaceRecommendations() + .then(() => this._allWorkspaceRecommendedExtensions.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId))); + } + + private fetchWorkspaceRecommendations(): TPromise { + + if (!this.isEnabled) { return TPromise.as(null); } + + return this.fetchExtensionRecommendationContents() + .then(result => this.validateExtensions(result.map(({ contents }) => contents)) + .then(({ invalidExtensions, message }) => { + + if (invalidExtensions.length > 0 && this.notificationService) { + this.notificationService.warn(`The below ${invalidExtensions.length} extension(s) in workspace recommendations have issues:\n${message}`); + } + + const seenUnWantedRecommendations: { [id: string]: boolean } = {}; + + this._allWorkspaceRecommendedExtensions = []; + this._workspaceIgnoredRecommendations = []; + + for (const contentsBySource of result) { + if (contentsBySource.contents.unwantedRecommendations) { + for (const r of contentsBySource.contents.unwantedRecommendations) { + const unwantedRecommendation = r.toLowerCase(); + if (!seenUnWantedRecommendations[unwantedRecommendation] && invalidExtensions.indexOf(unwantedRecommendation) === -1) { + this._workspaceIgnoredRecommendations.push(unwantedRecommendation); + seenUnWantedRecommendations[unwantedRecommendation] = true; + } + } + } + + if (contentsBySource.contents.recommendations) { + for (const r of contentsBySource.contents.recommendations) { + const extensionId = r.toLowerCase(); + if (invalidExtensions.indexOf(extensionId) === -1) { + let recommendation = this._allWorkspaceRecommendedExtensions.filter(r => r.extensionId === extensionId)[0]; + if (!recommendation) { + recommendation = { extensionId, sources: [] }; + this._allWorkspaceRecommendedExtensions.push(recommendation); + } + if (recommendation.sources.indexOf(contentsBySource.source) === -1) { + recommendation.sources.push(contentsBySource.source); + } + } + } + } + } + this._allIgnoredRecommendations = distinct([...this._globallyIgnoredRecommendations, ...this._workspaceIgnoredRecommendations]); + })); + } + + private fetchExtensionRecommendationContents(): TPromise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> { const workspace = this.contextService.getWorkspace(); - return TPromise.join([this.resolveWorkspaceRecommendations(workspace), ...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderRecommendations(workspaceFolder))]) - .then(recommendations => { - this._allWorkspaceRecommendedExtensions = distinct(flatten(recommendations)); - return this._allWorkspaceRecommendedExtensions; - }); + return TPromise.join<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }>([ + this.resolveWorkspaceExtensionConfig(workspace).then(contents => contents ? { contents, source: workspace } : null), + ...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder).then(contents => contents ? { contents, source: workspaceFolder } : null)) + ]).then(contents => coalesce(contents)); } - private resolveWorkspaceRecommendations(workspace: IWorkspace): TPromise { - if (workspace.configuration) { - return this.fileService.resolveContent(workspace.configuration) - .then(content => this.processWorkspaceRecommendations(json.parse(content.value, [])['extensions']), err => []); + private resolveWorkspaceExtensionConfig(workspace: IWorkspace): TPromise { + if (!workspace.configuration) { + return TPromise.as(null); } - return TPromise.as([]); + + return this.fileService.resolveContent(workspace.configuration) + .then(content => (json.parse(content.value)['extensions']), err => null); } - private resolveWorkspaceFolderRecommendations(workspaceFolder: IWorkspaceFolder): TPromise { + private resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): TPromise { const extensionsJsonUri = workspaceFolder.toResource(paths.join('.vscode', 'extensions.json')); - return this.fileService.resolveFile(extensionsJsonUri).then(() => { - return this.fileService.resolveContent(extensionsJsonUri) - .then(content => this.processWorkspaceRecommendations(json.parse(content.value, [])), err => []); - }, err => []); + + return this.fileService.resolveFile(extensionsJsonUri) + .then(() => this.fileService.resolveContent(extensionsJsonUri)) + .then(content => json.parse(content.value), err => null); } - private processWorkspaceRecommendations(extensionsContent: IExtensionsContent): TPromise { + private async validateExtensions(contents: IExtensionsConfigContent[]): TPromise<{ invalidExtensions: string[], message: string }> { + const extensionsContent: IExtensionsConfigContent = { + recommendations: distinct(flatten(contents.map(content => content.recommendations || []))), + unwantedRecommendations: distinct(flatten(contents.map(content => content.unwantedRecommendations || []))) + }; + const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN); - if (extensionsContent && extensionsContent.recommendations && extensionsContent.recommendations.length) { - let countBadRecommendations = 0; - let badRecommendationsString = ''; - let filteredRecommendations = extensionsContent.recommendations.filter((element, position) => { - if (extensionsContent.recommendations.indexOf(element) !== position) { + const invalidExtensions = []; + let message = ''; + + const regexFilter = (ids: string[]) => { + return ids.filter((element, position) => { + if (ids.indexOf(element) !== position) { // This is a duplicate entry, it doesn't hurt anybody // but it shouldn't be sent in the gallery query return false; } else if (!regEx.test(element)) { - countBadRecommendations++; - badRecommendationsString += `${element} (bad format) Expected: .\n`; + invalidExtensions.push(element.toLowerCase()); + message += `${element} (bad format) Expected: .\n`; return false; } - return true; }); + }; - return this._galleryService.query({ names: filteredRecommendations }).then(pager => { - let page = pager.firstPage; - let validRecommendations = page.map(extension => { - return extension.identifier.id.toLowerCase(); - }); + const filteredWanted = regexFilter(extensionsContent.recommendations || []).map(x => x.toLowerCase()); - if (validRecommendations.length !== filteredRecommendations.length) { - filteredRecommendations.forEach(element => { + if (filteredWanted.length) { + try { + let validRecommendations = (await this._galleryService.query({ names: filteredWanted })).firstPage + .map(extension => extension.identifier.id.toLowerCase()); + + if (validRecommendations.length !== filteredWanted.length) { + filteredWanted.forEach(element => { if (validRecommendations.indexOf(element.toLowerCase()) === -1) { - countBadRecommendations++; - badRecommendationsString += `${element} (not found in marketplace)\n`; + invalidExtensions.push(element.toLowerCase()); + message += `${element} (not found in marketplace)\n`; } }); } - - if (countBadRecommendations > 0 && this.notificationService) { - this.notificationService.warn( - 'The below ' + - countBadRecommendations + - ' extension(s) in workspace recommendations have issues:\n' + - badRecommendationsString - ); - } - - return validRecommendations; - }); + } catch (e) { + console.warn('Error querying extensions gallery', e); + } } + return { invalidExtensions, message }; + } - return TPromise.as([]); - + private isExtensionAllowedToBeRecommended(id: string): boolean { + return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1; } private onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): void { if (event.added.length) { - TPromise.join(event.added.map(workspaceFolder => this.resolveWorkspaceFolderRecommendations(workspaceFolder))) - .then(result => { - const newRecommendations = flatten(result); - // Suggest only if atleast one of the newly added recommendtations was not suggested before - if (newRecommendations.some(e => this._allWorkspaceRecommendedExtensions.indexOf(e) === -1)) { - this._suggestWorkspaceRecommendations(); + const oldWorkspaceRecommended = this._allWorkspaceRecommendedExtensions; + this.getWorkspaceRecommendations() + .then(currentWorkspaceRecommended => { + // Suggest only if at least one of the newly added recommendations was not suggested before + if (currentWorkspaceRecommended.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) { + this.promptWorkspaceRecommendations(); } }); } this._dynamicWorkspaceRecommendations = []; } - getFileBasedRecommendations(): string[] { - const fileBased = Object.keys(this._fileBasedRecommendations) + getFileBasedRecommendations(): IExtensionRecommendation[] { + return Object.keys(this._fileBasedRecommendations) .sort((a, b) => { - if (this._fileBasedRecommendations[a] === this._fileBasedRecommendations[b]) { - if (!product.extensionImportantTips || product.extensionImportantTips[a]) { + if (this._fileBasedRecommendations[a].recommendedTime === this._fileBasedRecommendations[b].recommendedTime) { + if (!product.extensionImportantTips || caseInsensitiveGet(product.extensionImportantTips, a)) { return -1; } - if (product.extensionImportantTips[b]) { + if (caseInsensitiveGet(product.extensionImportantTips, b)) { return 1; } } - return this._fileBasedRecommendations[a] > this._fileBasedRecommendations[b] ? -1 : 1; - }); - return fileBased; + return this._fileBasedRecommendations[a].recommendedTime > this._fileBasedRecommendations[b].recommendedTime ? -1 : 1; + }) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)) + .map(extensionId => ({ extensionId, sources: this._fileBasedRecommendations[extensionId].sources })); } - getOtherRecommendations(): TPromise { + getOtherRecommendations(): TPromise { return this.fetchProactiveRecommendations().then(() => { - const others = distinct([...Object.keys(this._exeBasedRecommendations), ...this._dynamicWorkspaceRecommendations]); - shuffle(others); - return others; + const others = distinct([ + ...Object.keys(this._exeBasedRecommendations), + ...this._dynamicWorkspaceRecommendations, + ...Object.keys(this._experimentalRecommendations), + ]).filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + shuffle(others, this.sessionSeed); + return others.map(extensionId => { + const sources: ExtensionRecommendationSource[] = []; + if (this._exeBasedRecommendations[extensionId]) { + sources.push('executable'); + } + if (this._dynamicWorkspaceRecommendations.indexOf(extensionId) !== -1) { + sources.push('dynamic'); + } + return ({ extensionId, sources }); + }); }); } - getKeymapRecommendations(): string[] { - return product.keymapExtensionTips || []; + getKeymapRecommendations(): IExtensionRecommendation[] { + return (product.keymapExtensionTips || []) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)) + .map(extensionId => ({ extensionId, sources: ['application'] })); } - private _suggestFileBasedRecommendations() { + getAllRecommendations(): TPromise { + if (!this.proactiveRecommendationsFetched) { + return TPromise.as([]); + } + return TPromise.join([ + this.getWorkspaceRecommendations(), + TPromise.as(this.getFileBasedRecommendations()), + this.getOtherRecommendations(), + TPromise.as(this.getKeymapRecommendations()) + ]).then(result => flatten(result).filter(e => this.isExtensionAllowedToBeRecommended(e.extensionId))); + } + + private fetchFileBasedRecommendations() { const extensionTips = product.extensionTips; if (!extensionTips) { return; @@ -297,9 +446,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let { key: id, value: pattern } = entry; let ids = this._availableRecommendations[pattern]; if (!ids) { - this._availableRecommendations[pattern] = [id]; + this._availableRecommendations[pattern] = [id.toLowerCase()]; } else { - ids.push(id); + ids.push(id.toLowerCase()); } }); @@ -308,16 +457,13 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe const { pattern } = value; let ids = this._availableRecommendations[pattern]; if (!ids) { - this._availableRecommendations[pattern] = [id]; + this._availableRecommendations[pattern] = [id.toLowerCase()]; } else { - ids.push(id); + ids.push(id.toLowerCase()); } }); - const allRecommendations: string[] = []; - forEach(this._availableRecommendations, ({ value: ids }) => { - allRecommendations.push(...ids); - }); + const allRecommendations: string[] = flatten((Object.keys(this._availableRecommendations).map(key => this._availableRecommendations[key]))); // retrieve ids of previous recommendations const storedRecommendationsJson = JSON.parse(this.storageService.get('extensionsAssistant/recommendations', StorageScope.GLOBAL, '[]')); @@ -325,7 +471,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (Array.isArray(storedRecommendationsJson)) { for (let id of storedRecommendationsJson) { if (allRecommendations.indexOf(id) > -1) { - this._fileBasedRecommendations[id] = Date.now(); + this._fileBasedRecommendations[id.toLowerCase()] = { recommendedTime: Date.now(), sources: ['cached'] }; } } } else { @@ -334,14 +480,11 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (typeof entry.value === 'number') { const diff = (now - entry.value) / milliSecondsInADay; if (diff <= 7 && allRecommendations.indexOf(entry.key) > -1) { - this._fileBasedRecommendations[entry.key] = entry.value; + this._fileBasedRecommendations[entry.key.toLowerCase()] = { recommendedTime: entry.value, sources: ['cached'] }; } } }); } - - this._modelService.onModelAdded(this._suggest, this, this._disposables); - this._modelService.getModels().forEach(model => this._suggest(model)); } private getMimeTypes(path: string): TPromise { @@ -350,15 +493,15 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); } - private _suggest(model: ITextModel): void { - const uri = model.uri; + private promptFiletypeBasedRecommendations(model: ITextModel): void { let hasSuggestion = false; - if (!uri || uri.scheme !== Schemas.file) { + const uri = model.uri; + if (!uri || !this.fileService.canHandleResource(uri)) { return; } - let fileExtension = paths.extname(uri.fsPath); + let fileExtension = paths.extname(uri.path); if (fileExtension) { if (processedFileExtensions.indexOf(fileExtension) > -1) { return; @@ -370,19 +513,28 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe // the critical path - in case glob-match is slow setImmediate(() => { + let recommendationsToSuggest: string[] = []; const now = Date.now(); forEach(this._availableRecommendations, entry => { let { key: pattern, value: ids } = entry; - if (match(pattern, uri.fsPath)) { + if (match(pattern, uri.path)) { for (let id of ids) { - this._fileBasedRecommendations[id] = now; + if (caseInsensitiveGet(product.extensionImportantTips, id)) { + recommendationsToSuggest.push(id); + } + const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] }; + filedBasedRecommendation.recommendedTime = now; + if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { + filedBasedRecommendation.sources.push(uri); + } + this._fileBasedRecommendations[id.toLowerCase()] = filedBasedRecommendation; } } }); this.storageService.store( 'extensionsAssistant/recommendations', - JSON.stringify(this._fileBasedRecommendations), + JSON.stringify(Object.keys(this._fileBasedRecommendations).reduce((result, key) => { result[key] = this._fileBasedRecommendations[key].recommendedTime; return result; }, {})), StorageScope.GLOBAL ); @@ -392,16 +544,16 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } const importantRecommendationsIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]')); - let recommendationsToSuggest = Object.keys(product.extensionImportantTips || []) - .filter(id => importantRecommendationsIgnoreList.indexOf(id) === -1 && match(product.extensionImportantTips[id]['pattern'], uri.fsPath)); + recommendationsToSuggest = recommendationsToSuggest.filter(id => importantRecommendationsIgnoreList.indexOf(id) === -1 && this.isExtensionAllowedToBeRecommended(id)); - const importantTipsPromise = recommendationsToSuggest.length === 0 ? TPromise.as(null) : this.extensionsService.getInstalled(LocalExtensionType.User).then(local => { - recommendationsToSuggest = recommendationsToSuggest.filter(id => local.every(local => `${local.manifest.publisher}.${local.manifest.name}` !== id)); + const importantTipsPromise = recommendationsToSuggest.length === 0 ? TPromise.as(null) : this.extensionWorkbenchService.queryLocal().then(local => { + const localExtensions = local.map(e => e.id); + recommendationsToSuggest = recommendationsToSuggest.filter(id => localExtensions.every(local => local !== id.toLowerCase())); if (!recommendationsToSuggest.length) { return; } const id = recommendationsToSuggest[0]; - const name = product.extensionImportantTips[id]['name']; + const name = caseInsensitiveGet(product.extensionImportantTips, id)['name']; // Indicates we have a suggested extension via the whitelist hasSuggestion = true; @@ -423,10 +575,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'install', extensionId: name }); - - const installAction = this.instantiationService.createInstance(InstallRecommendedExtensionAction, id); - installAction.run(); - installAction.dispose(); + this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run(); } }, { label: localize('showRecommendations', "Show Recommendations"), @@ -460,7 +609,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name }); - this.ignoreExtensionRecommendations(); + this.promptIgnoreExtensionRecommendations(); } }], () => { @@ -522,8 +671,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); } }, { - label: choiceNever, - isSecondary: true, + label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension), run: () => { fileExtensionSuggestionIgnoreList.push(fileExtension); this.storageService.store( @@ -555,91 +703,92 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); } - private _suggestWorkspaceRecommendations(): TPromise { + private promptWorkspaceRecommendations(): void { const storageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; const config = this.configurationService.getValue(ConfigurationKey); + const filteredRecs = this._allWorkspaceRecommendedExtensions.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId)); - return this.getWorkspaceRecommendations().then(allRecommendations => { - if (!allRecommendations.length || config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand || this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) { - return; + if (filteredRecs.length === 0 + || config.ignoreRecommendations + || config.showRecommendationsOnlyOnDemand + || this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) { + return; + } + + this.extensionsService.getInstalled(LocalExtensionType.User).then(local => { + const recommendations = filteredRecs.filter(({ extensionId }) => local.every(local => !areSameExtensions({ id: extensionId }, { id: getGalleryExtensionIdFromLocal(local) }))); + + if (!recommendations.length) { + return TPromise.as(void 0); } - return this.extensionsService.getInstalled(LocalExtensionType.User).done(local => { - const recommendations = allRecommendations - .filter(id => local.every(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}` !== id)); - - if (!recommendations.length) { - return TPromise.as(void 0); - } - - return new TPromise(c => { - this.notificationService.prompt( - Severity.Info, - localize('workspaceRecommended', "This workspace has extension recommendations."), - [{ - label: localize('installAll', "Install All"), - run: () => { - /* __GDPR__ - "extensionWorkspaceRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All")); - installAllAction.run(); - installAllAction.dispose(); - - c(void 0); + return new TPromise(c => { + this.notificationService.prompt( + Severity.Info, + localize('workspaceRecommended', "This workspace has extension recommendations."), + [{ + label: localize('installAll', "Install All"), + run: () => { + /* __GDPR__ + "extensionWorkspaceRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } - }, { - label: localize('showRecommendations', "Show Recommendations"), - run: () => { - /* __GDPR__ - "extensionWorkspaceRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - showAction.run(); - showAction.dispose(); + const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All"), recommendations); + installAllAction.run(); + installAllAction.dispose(); - c(void 0); - } - }, { - label: choiceNever, - isSecondary: true, - run: () => { - /* __GDPR__ - "extensionWorkspaceRecommendations:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); - this.storageService.store(storageKey, true, StorageScope.WORKSPACE); - - c(void 0); - } - }], - () => { + c(void 0); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: () => { /* __GDPR__ "extensionWorkspaceRecommendations:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + + const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); + showAction.run(); + showAction.dispose(); c(void 0); } - ); - }); + }, { + label: choiceNever, + isSecondary: true, + run: () => { + /* __GDPR__ + "extensionWorkspaceRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); + this.storageService.store(storageKey, true, StorageScope.WORKSPACE); + + c(void 0); + } + }], + () => { + /* __GDPR__ + "extensionWorkspaceRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + + c(void 0); + } + ); }); }); } - private ignoreExtensionRecommendations() { + private promptIgnoreExtensionRecommendations() { this.notificationService.prompt( Severity.Info, localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), @@ -653,7 +802,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe ); } - private _suggestBasedOnExecutables(): TPromise { + private fetchExecutableRecommendations(): TPromise { const homeDir = os.homedir(); let foundExecutables: Set = new Set(); @@ -662,9 +811,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (exists && !foundExecutables.has(exeName)) { foundExecutables.add(exeName); (product.exeBasedExtensionTips[exeName]['recommendations'] || []) - .forEach(x => { + .forEach(extensionId => { if (product.exeBasedExtensionTips[exeName]['friendlyName']) { - this._exeBasedRecommendations[x] = product.exeBasedExtensionTips[exeName]['friendlyName']; + this._exeBasedRecommendations[extensionId.toLowerCase()] = product.exeBasedExtensionTips[exeName]['friendlyName']; } }); } @@ -706,7 +855,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } } - private getCachedDynamicWorkspaceRecommendations() { + private fetchCachedDynamicWorkspaceRecommendations() { if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER) { return; } @@ -734,8 +883,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } } - private getDynamicWorkspaceRecommendations(): TPromise { + private fetchDynamicWorkspaceRecommendations(): TPromise { if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER + || !this.fileService.canHandleResource(this.contextService.getWorkspace().folders[0].uri) || this._dynamicWorkspaceRecommendations.length || !this._extensionsRecommendationsUrl) { return TPromise.as(null); @@ -749,7 +899,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return null; } - return this.requestService.request({ type: 'GET', url: this._extensionsRecommendationsUrl }).then(context => { + return this.requestService.request({ type: 'GET', url: this._extensionsRecommendationsUrl }, CancellationToken.None).then(context => { if (context.res.statusCode !== 200) { return TPromise.as(null); } @@ -764,7 +914,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe for (let j = 0; j < allRecommendations.length && !foundRemote; j++) { if (Array.isArray(allRecommendations[j].remoteSet) && allRecommendations[j].remoteSet.indexOf(hashedRemotes[i]) > -1) { foundRemote = true; - this._dynamicWorkspaceRecommendations = allRecommendations[j].recommendations || []; + this._dynamicWorkspaceRecommendations = allRecommendations[j].recommendations.filter(id => this.isExtensionAllowedToBeRecommended(id)) || []; this.storageService.store(storageKey, JSON.stringify({ recommendations: this._dynamicWorkspaceRecommendations, timestamp: Date.now() @@ -784,32 +934,46 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); } + private fetchExperimentalRecommendations() { + this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations).then(experiments => { + (experiments || []).forEach(experiment => { + if (experiment.state === ExperimentState.Run && experiment.action.properties && Array.isArray(experiment.action.properties.recommendations) && experiment.action.properties.recommendationReason) { + experiment.action.properties.recommendations.forEach(id => { + this._experimentalRecommendations[id] = experiment.action.properties.recommendationReason; + }); + } + }); + }); + } + getKeywordsForExtension(extension: string): string[] { const keywords = product.extensionKeywords || {}; return keywords[extension] || []; } - getRecommendationsForExtension(extension: string): string[] { - const str = `.${extension}`; - const result = Object.create(null); - - forEach(product.extensionTips || empty, entry => { - let { key: id, value: pattern } = entry; - - if (match(pattern, str)) { - result[id] = true; + toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean) { + const lowerId = extensionId.toLowerCase(); + if (shouldIgnore) { + const reason = this.getAllRecommendationsWithReason()[lowerId]; + if (reason && reason.reasonId) { + /* __GDPR__ + "extensionsRecommendations:ignoreRecommendation" : { + "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('extensionsRecommendations:ignoreRecommendation', { id: extensionId, recommendationReason: reason.reasonId }); } - }); + } - forEach(product.extensionImportantTips || empty, entry => { - let { key: id, value } = entry; + this._globallyIgnoredRecommendations = shouldIgnore ? + distinct([...this._globallyIgnoredRecommendations, lowerId].map(id => id.toLowerCase())) : + this._globallyIgnoredRecommendations.filter(id => id !== lowerId); - if (match(value.pattern, str)) { - result[id] = true; - } - }); + this.storageService.store('extensionsAssistant/ignored_recommendations', JSON.stringify(this._globallyIgnoredRecommendations), StorageScope.GLOBAL); + this._allIgnoredRecommendations = distinct([...this._globallyIgnoredRecommendations, ...this._workspaceIgnoredRecommendations]); - return Object.keys(result); + this._onRecommendationChange.fire({ extensionId: extensionId, isRecommended: !shouldIgnore }); } dispose() { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index 369e7f89b08..a942e6f13bb 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -5,13 +5,12 @@ import 'vs/css!./media/extensions'; import { localize } from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionGalleryService, IExtensionTipsService, ExtensionsLabel, ExtensionsChannelId, PreferencesLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; +import { IExtensionTipsService, ExtensionsLabel, ExtensionsChannelId, PreferencesLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; + import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -23,7 +22,7 @@ import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, EnableAllAction, EnableAllWorkpsaceAction, DisableAllAction, DisableAllWorkpsaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, OpenExtensionsFolderAction, InstallVSIXAction, ReinstallAction -} from 'vs/workbench/parts/extensions/browser/extensionsActions'; +} from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { ExtensionEditor } from 'vs/workbench/parts/extensions/electron-browser/extensionEditor'; @@ -44,9 +43,8 @@ import { EditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensio import { ExtensionHostProfileService } from 'vs/workbench/parts/extensions/electron-browser/extensionProfileService'; // Singletons -registerSingleton(IExtensionGalleryService, ExtensionGalleryService); -registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); +registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -57,7 +55,7 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); Registry.as(OutputExtensions.OutputChannels) - .registerChannel(ExtensionsChannelId, ExtensionsLabel); + .registerChannel({ id: ExtensionsChannelId, label: ExtensionsLabel, log: false }); // Quickopen Registry.as(Extensions.Quickopen).registerQuickOpenHandler( @@ -204,18 +202,32 @@ Registry.as(ConfigurationExtensions.Configuration) properties: { 'extensions.autoUpdate': { type: 'boolean', - description: localize('extensionsAutoUpdate', "Automatically update extensions"), + description: localize('extensionsAutoUpdate', "When enabled, automatically installs updates for extensions. The updates are fetched from an online service."), default: true, - scope: ConfigurationScope.APPLICATION + scope: ConfigurationScope.APPLICATION, + tags: ['usesOnlineServices'] + }, + 'extensions.autoCheckUpdates': { + type: 'boolean', + description: localize('extensionsCheckUpdates', "When enabled, automatically checks extensions for updates. If an extension has an update, it is marked as outdated in the Extensions view. The updates are fetched from an online service."), + default: true, + scope: ConfigurationScope.APPLICATION, + tags: ['usesOnlineServices'] }, 'extensions.ignoreRecommendations': { type: 'boolean', - description: localize('extensionsIgnoreRecommendations', "If set to true, the notifications for extension recommendations will stop showing up."), + description: localize('extensionsIgnoreRecommendations', "When enabled, the notifications for extension recommendations will not be shown."), default: false }, 'extensions.showRecommendationsOnlyOnDemand': { type: 'boolean', - description: localize('extensionsShowRecommendationsOnlyOnDemand', "If set to true, recommendations will not be fetched or shown unless specifically requested by the user."), + description: localize('extensionsShowRecommendationsOnlyOnDemand', "When enabled, recommendations will not be fetched or shown unless specifically requested by the user. Some recommendations are fetched from an online service."), + default: false, + tags: ['usesOnlineServices'] + }, + 'extensions.closeExtensionDetailsOnViewChange': { + type: 'boolean', + description: localize('extensionsCloseExtensionDetailsOnViewChange', "When enabled, editors with extension details will be automatically closed upon navigating away from the Extensions View."), default: false } } @@ -229,6 +241,37 @@ CommandsRegistry.registerCommand('_extensions.manage', (accessor: ServicesAccess const extensionService = accessor.get(IExtensionsWorkbenchService); const extension = extensionService.local.filter(e => areSameExtensions(e, { id: extensionId })); if (extension.length === 1) { - extensionService.open(extension[0]).done(null, errors.onUnexpectedError); + extensionService.open(extension[0]); } -}); \ No newline at end of file +}); + +// File menu registration + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: ShowRecommendedKeymapExtensionsAction.ID, + title: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '1_settings', + command: { + id: VIEWLET_ID, + title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") + }, + order: 2 +}); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") + }, + order: 5 +}); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index 06d3286942b..8a9c33a3e04 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -3,20 +3,2226 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/extensionActions'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Action } from 'vs/base/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; +import { Throttler } from 'vs/base/common/async'; +import severity from 'vs/base/common/severity'; +import * as DOM from 'vs/base/browser/dom'; import * as paths from 'vs/base/common/paths'; -import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/extensions/common/extensions'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; -import { IFileService } from 'vs/platform/files/common/files'; -import URI from 'vs/base/common/uri'; -import Severity from 'vs/base/common/severity'; +import { Event } from 'vs/base/common/event'; +import * as json from 'vs/base/common/json'; +import { ActionItem, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +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 } from 'vs/workbench/parts/extensions/common/extensions'; +import { ExtensionsConfigurationInitialContent } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate'; +import { LocalExtensionType, IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ToggleViewletAction } from 'vs/workbench/browser/viewlet'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; +import { IFileService, IContent } from 'vs/platform/files/common/files'; +import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IExtensionService, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { Color } from 'vs/base/common/color'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { PagedModel } from 'vs/base/common/paging'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; +import product from 'vs/platform/node/product'; +import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +const promptDownloadManually = (extension: IGalleryExtension, message: string, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService) => { + const downloadUrl = `${product.extensionsGallery.serviceUrl}/publishers/${extension.publisher}/vsextensions/${extension.name}/${extension.version}/vspackage`; + notificationService.prompt(Severity.Error, message, [{ + label: localize('download', "Download Manually"), + run: () => openerService.open(URI.parse(downloadUrl)).then(() => { + notificationService.prompt( + Severity.Info, + localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', extension.identifier.id), + [{ + label: InstallVSIXAction.LABEL, + run: () => { + const action = instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); + action.run(); + action.dispose(); + } + }] + ); + }) + }]); +}; + +export interface IExtensionAction extends IAction { + extension: IExtension; +} + +export class InstallAction extends Action { + + private static INSTALL_LABEL = localize('install', "Install"); + private static INSTALLING_LABEL = localize('installing', "Installing"); + + private static readonly Class = 'extension-action prominent install'; + private static readonly InstallingClass = 'extension-action install installing'; + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService private instantiationService: IInstantiationService, + @INotificationService private notificationService: INotificationService, + @IOpenerService private openerService: IOpenerService + ) { + super(`extensions.install`, InstallAction.INSTALL_LABEL, InstallAction.Class, false); + + this.disposables.push(this.extensionsWorkbenchService.onChange(extension => this.update(extension))); + this.update(); + } + + private update(extension?: IExtension): void { + if (!this.extension || this.extension.type === LocalExtensionType.System) { + this.enabled = false; + this.class = InstallAction.Class; + this.label = InstallAction.INSTALL_LABEL; + return; + } + + if (extension && !areSameExtensions(this.extension, extension)) { + return; + } + + this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && this.extension.state === ExtensionState.Uninstalled; + + if (this.extension.state === ExtensionState.Installing) { + this.label = InstallAction.INSTALLING_LABEL; + this.class = InstallAction.InstallingClass; + this.tooltip = InstallAction.INSTALLING_LABEL; + } else { + this.label = InstallAction.INSTALL_LABEL; + this.class = InstallAction.Class; + this.tooltip = InstallAction.INSTALL_LABEL; + } + } + + run(): TPromise { + this.extensionsWorkbenchService.open(this.extension); + + return this.install(this.extension); + } + + private install(extension: IExtension): TPromise { + return this.extensionsWorkbenchService.install(extension).then(null, err => { + if (!extension.gallery) { + return this.notificationService.error(err); + } + + console.error(err); + + promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); + }); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class UninstallAction extends Action { + + private static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); + private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); + + private static readonly UninstallClass = 'extension-action uninstall'; + private static readonly UnInstallingClass = 'extension-action uninstall uninstalling'; + + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super('extensions.uninstall', UninstallAction.UninstallLabel, UninstallAction.UninstallClass, false); + + this.disposables.push(this.extensionsWorkbenchService.onChange(extension => this.update(extension))); + this.update(); + } + + private update(extension?: IExtension): void { + if (!this.extension) { + this.enabled = false; + return; + } + + if (extension && !areSameExtensions(this.extension, extension)) { + return; + } + + const state = this.extension.state; + + if (state === ExtensionState.Uninstalling) { + this.label = UninstallAction.UninstallingLabel; + this.class = UninstallAction.UnInstallingClass; + this.enabled = false; + return; + } + + this.label = UninstallAction.UninstallLabel; + this.class = UninstallAction.UninstallClass; + + const installedExtensions = this.extensionsWorkbenchService.local.filter(e => e.id === this.extension.id); + + if (!installedExtensions.length) { + this.enabled = false; + return; + } + + if (installedExtensions[0].type !== LocalExtensionType.User) { + this.enabled = false; + return; + } + + this.enabled = true; + } + + run(): TPromise { + return this.extensionsWorkbenchService.uninstall(this.extension); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class CombinedInstallAction extends Action { + + private static readonly NoExtensionClass = 'extension-action prominent install no-extension'; + private installAction: InstallAction; + private uninstallAction: UninstallAction; + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { + this._extension = extension; + this.installAction.extension = extension; + this.uninstallAction.extension = extension; + } + + constructor( + @IInstantiationService instantiationService: IInstantiationService + ) { + super('extensions.combinedInstall', '', '', false); + + this.installAction = instantiationService.createInstance(InstallAction); + this.uninstallAction = instantiationService.createInstance(UninstallAction); + this.disposables.push(this.installAction, this.uninstallAction); + + this.installAction.onDidChange(this.update, this, this.disposables); + this.uninstallAction.onDidChange(this.update, this, this.disposables); + this.update(); + } + + private update(): void { + if (!this.extension || this.extension.type === LocalExtensionType.System) { + this.enabled = false; + this.class = CombinedInstallAction.NoExtensionClass; + } else if (this.installAction.enabled) { + this.enabled = true; + this.label = this.installAction.label; + this.class = this.installAction.class; + this.tooltip = this.installAction.tooltip; + } else if (this.uninstallAction.enabled) { + this.enabled = true; + this.label = this.uninstallAction.label; + this.class = this.uninstallAction.class; + this.tooltip = this.uninstallAction.tooltip; + } else if (this.extension.state === ExtensionState.Installing) { + this.enabled = false; + this.label = this.installAction.label; + this.class = this.installAction.class; + this.tooltip = this.installAction.tooltip; + } else if (this.extension.state === ExtensionState.Uninstalling) { + this.enabled = false; + this.label = this.uninstallAction.label; + this.class = this.uninstallAction.class; + this.tooltip = this.uninstallAction.tooltip; + } else { + this.enabled = false; + this.label = this.installAction.label; + this.class = this.installAction.class; + this.tooltip = this.installAction.tooltip; + } + } + + run(): TPromise { + if (this.installAction.enabled) { + return this.installAction.run(); + } else if (this.uninstallAction.enabled) { + return this.uninstallAction.run(); + } + + return TPromise.as(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class UpdateAction extends Action { + + private static readonly EnabledClass = 'extension-action prominent update'; + private static readonly DisabledClass = `${UpdateAction.EnabledClass} disabled`; + + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService private instantiationService: IInstantiationService, + @INotificationService private notificationService: INotificationService, + @IOpenerService private openerService: IOpenerService + ) { + super(`extensions.update`, '', UpdateAction.DisabledClass, false); + this.disposables.push(this.extensionsWorkbenchService.onChange(extension => this.update(extension))); + this.update(); + } + + private update(extension?: IExtension): void { + if (!this.extension) { + this.enabled = false; + this.class = UpdateAction.DisabledClass; + this.label = this.getUpdateLabel(); + return; + } + + if (extension && !areSameExtensions(this.extension, extension)) { + return; + } + + if (this.extension.type !== LocalExtensionType.User) { + this.enabled = false; + this.class = UpdateAction.DisabledClass; + this.label = this.getUpdateLabel(); + return; + } + + const canInstall = this.extensionsWorkbenchService.canInstall(this.extension); + const isInstalled = this.extension.state === ExtensionState.Installed; + + this.enabled = canInstall && isInstalled && this.extension.outdated; + this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; + this.label = this.extension.outdated ? this.getUpdateLabel(this.extension.latestVersion) : this.getUpdateLabel(); + } + + run(): TPromise { + return this.install(this.extension); + } + + private install(extension: IExtension): TPromise { + return this.extensionsWorkbenchService.install(extension).then(null, err => { + if (!extension.gallery) { + return this.notificationService.error(err); + } + + console.error(err); + + promptDownloadManually(extension.gallery, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); + }); + } + + private getUpdateLabel(version?: string): string { + return version ? localize('updateTo', "Update to {0}", version) : localize('updateAction', "Update"); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class DropDownMenuActionItem extends ActionItem { + + private disposables: IDisposable[] = []; + + private _menuActionGroups: IAction[][]; + get menuActionGroups(): IAction[][] { return this._menuActionGroups; } + set menuActionGroups(menuActionGroups: IAction[][]) { this._menuActionGroups = menuActionGroups; } + + constructor(action: IAction, menuActionGroups: IAction[][], + @IContextMenuService private contextMenuService: IContextMenuService + ) { + super(null, action, { icon: true, label: true }); + this.menuActionGroups = menuActionGroups; + } + + public showMenu(): void { + const actions = this.getActions(); + let elementPosition = DOM.getDomNodePagePosition(this.element); + const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => TPromise.wrap(actions), + actionRunner: this.actionRunner + }); + } + + private getActions(): IAction[] { + let actions: IAction[] = []; + const menuActionGroups = this.menuActionGroups; + for (const menuActions of menuActionGroups) { + actions = [...actions, ...menuActions, new Separator()]; + } + return actions.length ? actions.slice(0, actions.length - 1) : actions; + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class ManageExtensionAction extends Action { + + static readonly ID = 'extensions.manage'; + + private static readonly Class = 'extension-action manage'; + private static readonly HideManageExtensionClass = `${ManageExtensionAction.Class} hide`; + + private _actionItem: DropDownMenuActionItem; + get actionItem(): IActionItem { return this._actionItem; } + + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(ManageExtensionAction.ID); + + this.tooltip = localize('manage', "Manage"); + this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, this.createMenuActionGroups()); + this.disposables.push(this._actionItem); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private createMenuActionGroups(): IAction[][] { + const groups: IAction[][] = []; + groups.push([ + this.instantiationService.createInstance(EnableGloballyAction, EnableGloballyAction.LABEL), + this.instantiationService.createInstance(EnableForWorkspaceAction, EnableForWorkspaceAction.LABEL) + ]); + groups.push([ + this.instantiationService.createInstance(DisableGloballyAction, DisableGloballyAction.LABEL), + this.instantiationService.createInstance(DisableForWorkspaceAction, DisableForWorkspaceAction.LABEL) + ]); + groups.push([this.instantiationService.createInstance(UninstallAction)]); + return groups; + } + + private update(): void { + this.class = ManageExtensionAction.HideManageExtensionClass; + this.enabled = false; + if (this.extension) { + const state = this.extension.state; + this.enabled = state === ExtensionState.Installed; + this.class = this.enabled || state === ExtensionState.Uninstalling ? ManageExtensionAction.Class : ManageExtensionAction.HideManageExtensionClass; + this.tooltip = state === ExtensionState.Uninstalling ? localize('ManageExtensionAction.uninstallingTooltip', "Uninstalling") : ''; + } + const menuActionGroups = this.createMenuActionGroups(); + for (const actions of menuActionGroups) { + for (const action of actions) { + (action).extension = this.extension; + } + } + this._actionItem.menuActionGroups = menuActionGroups; + } + + public run(): TPromise { + this._actionItem.showMenu(); + return TPromise.wrap(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class EnableForWorkspaceAction extends Action implements IExtensionAction { + + static readonly ID = 'extensions.enableForWorkspace'; + static LABEL = localize('enableForWorkspaceAction', "Enable (Workspace)"); + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(label: string, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(EnableForWorkspaceAction.ID, label); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.disposables.push(this.workspaceContextService.onDidChangeWorkbenchState(() => this.update())); + this.update(); + } + + private update(): void { + this.enabled = false; + if (this.extension) { + this.enabled = (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); + } + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.WorkspaceEnabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class EnableGloballyAction extends Action implements IExtensionAction { + + static readonly ID = 'extensions.enableGlobally'; + static LABEL = localize('enableGloballyAction', "Enable"); + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(label: string, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(EnableGloballyAction.ID, label); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + this.enabled = false; + if (this.extension) { + this.enabled = (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); + } + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.Enabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class EnableAction extends Action { + + static readonly ID = 'extensions.enable'; + private static readonly EnabledClass = 'extension-action prominent enable'; + private static readonly DisabledClass = `${EnableAction.EnabledClass} disabled`; + + private disposables: IDisposable[] = []; + + private _enableActions: IExtensionAction[]; + + private _actionItem: DropDownMenuActionItem; + get actionItem(): IActionItem { return this._actionItem; } + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(EnableAction.ID, localize('enableAction', "Enable"), EnableAction.DisabledClass, false); + + this._enableActions = [ + instantiationService.createInstance(EnableGloballyAction, EnableGloballyAction.LABEL), + instantiationService.createInstance(EnableForWorkspaceAction, EnableForWorkspaceAction.LABEL) + ]; + this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, [this._enableActions]); + this.disposables.push(this._actionItem); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + for (const actions of this._actionItem.menuActionGroups) { + for (const action of actions) { + (action).extension = this.extension; + } + } + + if (!this.extension) { + this.enabled = false; + this.class = EnableAction.DisabledClass; + return; + } + + this.enabled = this.extension.state === ExtensionState.Installed && this._enableActions.some(e => e.enabled); + this.class = this.enabled ? EnableAction.EnabledClass : EnableAction.DisabledClass; + } + + public run(): TPromise { + this._actionItem.showMenu(); + return TPromise.wrap(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } + +} + +export class DisableForWorkspaceAction extends Action implements IExtensionAction { + + static readonly ID = 'extensions.disableForWorkspace'; + static LABEL = localize('disableForWorkspaceAction', "Disable (Workspace)"); + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(label: string, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(DisableForWorkspaceAction.ID, label); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); + } + + private update(): void { + this.enabled = false; + if (this.extension && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + this.enabled = (this.extension.enablementState === EnablementState.Enabled || this.extension.enablementState === EnablementState.WorkspaceEnabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); + } + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.WorkspaceDisabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class DisableGloballyAction extends Action implements IExtensionAction { + + static readonly ID = 'extensions.disableGlobally'; + static LABEL = localize('disableGloballyAction', "Disable"); + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(label: string, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(DisableGloballyAction.ID, label); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + this.enabled = false; + if (this.extension) { + this.enabled = (this.extension.enablementState === EnablementState.Enabled || this.extension.enablementState === EnablementState.WorkspaceEnabled) && this.extension.local && this.extensionEnablementService.canChangeEnablement(this.extension.local); + } + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extension, EnablementState.Disabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class DisableAction extends Action { + + static readonly ID = 'extensions.disable'; + + private static readonly EnabledClass = 'extension-action disable'; + private static readonly DisabledClass = `${DisableAction.EnabledClass} disabled`; + + private disposables: IDisposable[] = []; + private _disableActions: IExtensionAction[]; + private _actionItem: DropDownMenuActionItem; + get actionItem(): IActionItem { return this._actionItem; } + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + ) { + super(DisableAction.ID, localize('disableAction', "Disable"), DisableAction.DisabledClass, false); + this._disableActions = [ + instantiationService.createInstance(DisableGloballyAction, DisableGloballyAction.LABEL), + instantiationService.createInstance(DisableForWorkspaceAction, DisableForWorkspaceAction.LABEL) + ]; + this._actionItem = this.instantiationService.createInstance(DropDownMenuActionItem, this, [this._disableActions]); + this.disposables.push(this._actionItem); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + for (const actions of this._actionItem.menuActionGroups) { + for (const action of actions) { + (action).extension = this.extension; + } + } + + if (!this.extension) { + this.enabled = false; + this.class = DisableAction.DisabledClass; + return; + } + + this.enabled = this.extension.state === ExtensionState.Installed && this._disableActions.some(a => a.enabled); + this.class = this.enabled ? DisableAction.EnabledClass : DisableAction.DisabledClass; + } + + public run(): TPromise { + this._actionItem.showMenu(); + return TPromise.wrap(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class CheckForUpdatesAction extends Action { + + static readonly ID = 'workbench.extensions.action.checkForUpdates'; + static LABEL = localize('checkForUpdates', "Check for Extension Updates"); + + constructor( + id = CheckForUpdatesAction.ID, + label = CheckForUpdatesAction.LABEL, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IViewletService private viewletService: IViewletService, + @INotificationService private notificationService: INotificationService + ) { + super(id, label, '', true); + } + + private checkUpdatesAndNotify(): void { + this.extensionsWorkbenchService.queryLocal().then( + extensions => { + const outdatedCount = extensions.filter(ext => ext.outdated === true).length; + let msgAvailableExtensions = localize('noUpdatesAvailable', "All Extensions are up to date."); + if (outdatedCount > 0) { + msgAvailableExtensions = outdatedCount === 1 ? localize('updateAvailable', "An Extension update is available.") + : localize('updatesAvailable', "{0} extensions updates are available.", outdatedCount); + this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => viewlet.search('')); + } + this.notificationService.notify({ severity: severity.Info, message: msgAvailableExtensions }); + } + ); + } + + run(): TPromise { + return this.extensionsWorkbenchService.checkForUpdates().then(() => this.checkUpdatesAndNotify()); + } +} + +export class ToggleAutoUpdateAction extends Action { + + constructor( + id: string, + label: string, + private autoUpdateValue: boolean, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(id, label, '', true); + this.updateEnablement(); + configurationService.onDidChangeConfiguration(() => this.updateEnablement()); + } + + private updateEnablement(): void { + this.enabled = this.configurationService.getValue(AutoUpdateConfigurationKey) !== this.autoUpdateValue; + } + + run(): TPromise { + return this.configurationService.updateValue(AutoUpdateConfigurationKey, this.autoUpdateValue); + } +} + +export class EnableAutoUpdateAction extends ToggleAutoUpdateAction { + + static readonly ID = 'workbench.extensions.action.enableAutoUpdate'; + static LABEL = localize('enableAutoUpdate', "Enable Auto Updating Extensions"); + + constructor( + id = EnableAutoUpdateAction.ID, + label = EnableAutoUpdateAction.LABEL, + @IConfigurationService configurationService: IConfigurationService + ) { + super(id, label, true, configurationService); + } +} + +export class DisableAutoUpdateAction extends ToggleAutoUpdateAction { + + static readonly ID = 'workbench.extensions.action.disableAutoUpdate'; + static LABEL = localize('disableAutoUpdate', "Disable Auto Updating Extensions"); + + constructor( + id = EnableAutoUpdateAction.ID, + label = EnableAutoUpdateAction.LABEL, + @IConfigurationService configurationService: IConfigurationService + ) { + super(id, label, false, configurationService); + } +} + +export class UpdateAllAction extends Action { + + static readonly ID = 'workbench.extensions.action.updateAllExtensions'; + static LABEL = localize('updateAll', "Update All Extensions"); + + private disposables: IDisposable[] = []; + + constructor( + id = UpdateAllAction.ID, + label = UpdateAllAction.LABEL, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @INotificationService private notificationService: INotificationService, + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService + ) { + super(id, label, '', false); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private get outdated(): IExtension[] { + return this.extensionsWorkbenchService.local.filter(e => e.outdated && e.state !== ExtensionState.Installing); + } + + private update(): void { + this.enabled = this.outdated.length > 0; + } + + run(): TPromise { + return TPromise.join(this.outdated.map(e => this.install(e))); + } + + private install(extension: IExtension): TPromise { + return this.extensionsWorkbenchService.install(extension).then(null, err => { + if (!extension.gallery) { + return this.notificationService.error(err); + } + + console.error(err); + + promptDownloadManually(extension.gallery, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); + }); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class ReloadAction extends Action { + + private static readonly EnabledClass = 'extension-action reload'; + private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; + + private disposables: IDisposable[] = []; + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + reloadMessage: string = ''; + private throttler: Throttler; + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWindowService private windowService: IWindowService, + @IExtensionService private extensionService: IExtensionService, + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); + this.throttler = new Throttler(); + + this.disposables.push(this.extensionsWorkbenchService.onChange(extension => this.update(extension))); + this.update(); + } + + private update(extension?: IExtension): void { + if (extension && this.extension && !areSameExtensions(this.extension, extension)) { + return; + } + this.throttler.queue(() => { + this.enabled = false; + this.tooltip = ''; + this.reloadMessage = ''; + if (!this.extension) { + return TPromise.wrap(null); + } + const state = this.extension.state; + if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { + return TPromise.wrap(null); + } + return this.extensionService.getExtensions() + .then(runningExtensions => this.computeReloadState(runningExtensions)); + }).then(() => { + this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; + }); + } + + private computeReloadState(runningExtensions: IExtensionDescription[]): void { + const installed = this.extensionsWorkbenchService.local.filter(e => e.id === this.extension.id)[0]; + const isUninstalled = this.extension.state === ExtensionState.Uninstalled; + const isDisabled = this.extension.local ? !this.extensionEnablementService.isEnabled(this.extension.local) : false; + const runningExtension = runningExtensions.filter(e => areSameExtensions(e, this.extension))[0]; + + if (installed && installed.local) { + if (runningExtension) { + const isDifferentVersionRunning = this.extension.version !== runningExtension.version; + if (isDifferentVersionRunning && !isDisabled) { + // Requires reload to run the updated extension + this.enabled = true; + this.tooltip = localize('postUpdateTooltip', "Reload to update"); + this.reloadMessage = localize('postUpdateMessage', "Reload this window to activate the updated extension '{0}'?", this.extension.displayName); + return; + } + if (isDisabled) { + // Requires reload to disable the extension + this.enabled = true; + this.tooltip = localize('postDisableTooltip', "Reload to deactivate"); + this.reloadMessage = localize('postDisableMessage', "Reload this window to deactivate the extension '{0}'?", this.extension.displayName); + return; + } + } else { + const extensionServer = this.extensionManagementServerService.getExtensionManagementServer(installed.local.location); + const localServer = this.extensionManagementServerService.getLocalExtensionManagementServer(); + // Only extension from local server requires reload if it is not running on the server + if (extensionServer && extensionServer.authority === localServer.authority && !isDisabled) { + // Requires reload to enable the extension + this.enabled = true; + this.tooltip = localize('postEnableTooltip', "Reload to activate"); + this.reloadMessage = localize('postEnableMessage', "Reload this window to activate the extension '{0}'?", this.extension.displayName); + return; + } + } + return; + } + + if (isUninstalled && runningExtension) { + // Requires reload to deactivate the extension + this.enabled = true; + this.tooltip = localize('postUninstallTooltip', "Reload to deactivate"); + this.reloadMessage = localize('postUninstallMessage', "Reload this window to deactivate the uninstalled extension '{0}'?", this.extension.displayName); + return; + } + } + + run(): TPromise { + return this.windowService.reloadWindow(); + } +} + +export class OpenExtensionsViewletAction extends ToggleViewletAction { + + static ID = VIEWLET_ID; + static LABEL = localize('toggleExtensionsViewlet', "Show Extensions"); + + constructor( + id: string, + label: string, + @IViewletService viewletService: IViewletService, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, VIEWLET_ID, viewletService, editorGroupService); + } +} + +export class InstallExtensionsAction extends OpenExtensionsViewletAction { + static ID = 'workbench.extensions.action.installExtensions'; + static LABEL = localize('installExtensions', "Install Extensions"); +} + +export class ShowEnabledExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showEnabledExtensions'; + static LABEL = localize('showEnabledExtensions', 'Show Enabled Extensions'); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@enabled '); + viewlet.focus(); + }); + } +} + +export class ShowInstalledExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showInstalledExtensions'; + static LABEL = localize('showInstalledExtensions', "Show Installed Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@installed '); + viewlet.focus(); + }); + } +} + +export class ShowDisabledExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showDisabledExtensions'; + static LABEL = localize('showDisabledExtensions', "Show Disabled Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, 'null', true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@disabled '); + viewlet.focus(); + }); + } +} + +export class ClearExtensionsInputAction extends Action { + + static readonly ID = 'workbench.extensions.action.clearExtensionsInput'; + static LABEL = localize('clearExtensionsInput', "Clear Extensions Input"); + + private disposables: IDisposable[] = []; + + constructor( + id: string, + label: string, + onSearchChange: Event, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, 'clear-extensions', true); + this.enabled = false; + onSearchChange(this.onSearchChange, this, this.disposables); + } + + private onSearchChange(value: string): void { + this.enabled = !!value; + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(''); + viewlet.focus(); + }); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +export class ShowBuiltInExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.listBuiltInExtensions'; + static LABEL = localize('showBuiltInExtensions', "Show Built-in Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@builtin '); + viewlet.focus(); + }); + } +} + +export class ShowOutdatedExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.listOutdatedExtensions'; + static LABEL = localize('showOutdatedExtensions', "Show Outdated Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@outdated '); + viewlet.focus(); + }); + } +} + +export class ShowPopularExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showPopularExtensions'; + static LABEL = localize('showPopularExtensions', "Show Popular Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@sort:installs '); + viewlet.focus(); + }); + } +} + +export class ShowRecommendedExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; + static LABEL = localize('showRecommendedExtensions', "Show Recommended Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@recommended '); + viewlet.focus(); + }); + } +} + +export class InstallWorkspaceRecommendedExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions'; + static LABEL = localize('installWorkspaceRecommendedExtensions', "Install All Workspace Recommended Extensions"); + + private _recommendations: IExtensionRecommendation[] = []; + get recommendations(): IExtensionRecommendation[] { return this._recommendations; } + set recommendations(recommendations: IExtensionRecommendation[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; } + + constructor( + id: string = InstallWorkspaceRecommendedExtensionsAction.ID, + label: string = InstallWorkspaceRecommendedExtensionsAction.LABEL, + recommendations: IExtensionRecommendation[], + @IViewletService private viewletService: IViewletService, + @INotificationService private notificationService: INotificationService, + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService, + @IExtensionsWorkbenchService private extensionWorkbenchService: IExtensionsWorkbenchService + ) { + super(id, label, 'extension-action'); + this.recommendations = recommendations; + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@recommended '); + viewlet.focus(); + const names = this.recommendations.map(({ extensionId }) => extensionId); + return this.extensionWorkbenchService.queryGallery({ names, source: 'install-all-workspace-recommendations' }).then(pager => { + let installPromises = []; + let model = new PagedModel(pager); + for (let i = 0; i < pager.total; i++) { + installPromises.push(model.resolve(i, CancellationToken.None).then(e => { + return this.extensionWorkbenchService.install(e).then(null, err => { + console.error(err); + promptDownloadManually(e.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", e.id), this.instantiationService, this.notificationService, this.openerService); + }); + })); + } + return TPromise.join(installPromises); + }); + }); + } +} + +export class InstallRecommendedExtensionAction extends Action { + + static readonly ID = 'workbench.extensions.action.installRecommendedExtension'; + static LABEL = localize('installRecommendedExtension', "Install Recommended Extension"); + + private extensionId: string; + + constructor( + extensionId: string, + @IViewletService private viewletService: IViewletService, + @INotificationService private notificationService: INotificationService, + @IInstantiationService private instantiationService: IInstantiationService, + @IOpenerService private openerService: IOpenerService, + @IExtensionsWorkbenchService private extensionWorkbenchService: IExtensionsWorkbenchService + ) { + super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, null, false); + this.extensionId = extensionId; + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@recommended '); + viewlet.focus(); + return this.extensionWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation', pageSize: 1 }) + .then(pager => { + if (pager && pager.firstPage && pager.firstPage.length) { + const extension = pager.firstPage[0]; + return this.extensionWorkbenchService.install(extension) + .then(() => null, err => { + console.error(err); + promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), this.instantiationService, this.notificationService, this.openerService); + }); + } + return null; + }); + }); + } +} + +export class IgnoreExtensionRecommendationAction extends Action { + + static readonly ID = 'extensions.ignore'; + + private static readonly Class = 'extension-action ignore'; + + private disposables: IDisposable[] = []; + extension: IExtension; + + constructor( + @IExtensionTipsService private extensionsTipsService: IExtensionTipsService, + ) { + super(IgnoreExtensionRecommendationAction.ID, 'Ignore Recommendation'); + + this.class = IgnoreExtensionRecommendationAction.Class; + this.tooltip = localize('ignoreExtensionRecommendation', "Do not recommend this extension again"); + this.enabled = true; + } + + public run(): TPromise { + this.extensionsTipsService.toggleIgnoredRecommendation(this.extension.id, true); + return TPromise.as(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class UndoIgnoreExtensionRecommendationAction extends Action { + + static readonly ID = 'extensions.ignore'; + + private static readonly Class = 'extension-action undo-ignore'; + + private disposables: IDisposable[] = []; + extension: IExtension; + + constructor( + @IExtensionTipsService private extensionsTipsService: IExtensionTipsService, + ) { + super(UndoIgnoreExtensionRecommendationAction.ID, 'Undo'); + + this.class = UndoIgnoreExtensionRecommendationAction.Class; + this.tooltip = localize('undo', "Undo"); + this.enabled = true; + } + + public run(): TPromise { + this.extensionsTipsService.toggleIgnoredRecommendation(this.extension.id, false); + return TPromise.as(null); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + + +export class ShowRecommendedKeymapExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; + static SHORT_LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@recommended:keymaps '); + viewlet.focus(); + }); + } +} + +export class ShowLanguageExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showLanguageExtensions'; + static SHORT_LABEL = localize('showLanguageExtensionsShort', "Language Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@category:"programming languages" @sort:installs '); + viewlet.focus(); + }); + } +} + +export class ShowAzureExtensionsAction extends Action { + + static readonly ID = 'workbench.extensions.action.showAzureExtensions'; + static SHORT_LABEL = localize('showAzureExtensionsShort', "Azure Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@sort:installs azure '); + viewlet.focus(); + }); + } +} + +export class ChangeSortAction extends Action { + + private query: Query; + private disposables: IDisposable[] = []; + + constructor( + id: string, + label: string, + onSearchChange: Event, + private sortBy: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + + if (sortBy === undefined) { + throw new Error('bad arguments'); + } + + this.query = Query.parse(''); + this.enabled = false; + onSearchChange(this.onSearchChange, this, this.disposables); + } + + private onSearchChange(value: string): void { + const query = Query.parse(value); + this.query = new Query(query.value, this.sortBy || query.sortBy, query.groupBy); + this.enabled = value && this.query.isValid() && !this.query.equals(query); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(this.query.toString()); + viewlet.focus(); + }); + } +} + +export class ChangeGroupAction extends Action { + + private query: Query; + private disposables: IDisposable[] = []; + + constructor( + id: string, + label: string, + onSearchChange: Event, + private groupBy: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, 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(): TPromise { + 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); + private workspaceFolderContextKey = new RawContextKey('workspaceFolderRecommendations', true); + private addToWorkspaceRecommendationsContextKey = new RawContextKey('addToWorkspaceRecommendations', false); + private addToWorkspaceFolderRecommendationsContextKey = new RawContextKey('addToWorkspaceFolderRecommendations', false); + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IEditorService editorService: IEditorService + ) { + super(); + const boundWorkspaceContextKey = this.workspaceContextKey.bindTo(contextKeyService); + boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE); + this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); + + const boundWorkspaceFolderContextKey = this.workspaceFolderContextKey.bindTo(contextKeyService); + boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0); + this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0))); + + const boundAddToWorkspaceRecommendationsContextKey = this.addToWorkspaceRecommendationsContextKey.bindTo(contextKeyService); + boundAddToWorkspaceRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE); + this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceRecommendationsContextKey.set( + editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); + this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundAddToWorkspaceRecommendationsContextKey.set( + editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE))); + + const boundAddToWorkspaceFolderRecommendationsContextKey = this.addToWorkspaceFolderRecommendationsContextKey.bindTo(contextKeyService); + boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput); + this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput))); + + this.registerCommands(); + } + + private registerCommands(): void { + CommandsRegistry.registerCommand(ConfigureWorkspaceRecommendedExtensionsAction.ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ConfigureWorkspaceRecommendedExtensionsAction.ID, + title: `${ExtensionsLabel}: ${ConfigureWorkspaceRecommendedExtensionsAction.LABEL}`, + }, + when: this.workspaceContextKey + }); + + CommandsRegistry.registerCommand(ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, + title: `${ExtensionsLabel}: ${ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL}`, + }, + when: this.workspaceFolderContextKey + }); + + CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.ADD_ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService) + .createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.ADD_ID, AddToWorkspaceRecommendationsAction.ADD_LABEL) + .run(AddToWorkspaceRecommendationsAction.ADD); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: AddToWorkspaceRecommendationsAction.ADD_ID, + title: `${ExtensionsLabel}: ${AddToWorkspaceRecommendationsAction.ADD_LABEL}` + }, + when: this.addToWorkspaceRecommendationsContextKey + }); + + CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.ADD_ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService) + .createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.ADD_ID, AddToWorkspaceFolderRecommendationsAction.ADD_LABEL) + .run(AddToWorkspaceRecommendationsAction.ADD); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: AddToWorkspaceFolderRecommendationsAction.ADD_ID, + title: `${ExtensionsLabel}: ${AddToWorkspaceFolderRecommendationsAction.ADD_LABEL}` + }, + when: this.addToWorkspaceFolderRecommendationsContextKey + }); + + CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.IGNORE_ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService) + .createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.IGNORE_ID, AddToWorkspaceRecommendationsAction.IGNORE_LABEL) + .run(AddToWorkspaceRecommendationsAction.IGNORE); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: AddToWorkspaceRecommendationsAction.IGNORE_ID, + title: `${ExtensionsLabel}: ${AddToWorkspaceRecommendationsAction.IGNORE_LABEL}` + }, + when: this.addToWorkspaceRecommendationsContextKey + }); + + CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService) + .createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL) + .run(AddToWorkspaceRecommendationsAction.IGNORE); + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, + title: `${ExtensionsLabel}: ${AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL}` + }, + when: this.addToWorkspaceFolderRecommendationsContextKey + }); + } +} + +export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { + + constructor( + id: string, + label: string, + @IWorkspaceContextService protected contextService: IWorkspaceContextService, + @IFileService private fileService: IFileService, + @IEditorService protected editorService: IEditorService, + @IJSONEditingService private jsonEditingService: IJSONEditingService, + @ITextModelService private textModelResolverService: ITextModelService + ) { + super(id, label, null); + } + + protected openExtensionsFile(extensionsFileResource: URI): TPromise { + return this.getOrCreateExtensionsFile(extensionsFileResource) + .then(({ created, content }) => + this.getSelectionPosition(content, extensionsFileResource, ['recommendations']) + .then(selection => this.editorService.openEditor({ + resource: extensionsFileResource, + options: { + pinned: created, + selection + } + })), + error => TPromise.wrapError(new Error(localize('OpenExtensionsFile.failed', "Unable to create 'extensions.json' file inside the '.vscode' folder ({0}).", error)))); + } + + protected openWorkspaceConfigurationFile(workspaceConfigurationFile: URI): TPromise { + return this.getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile) + .then(content => this.getSelectionPosition(content.value, content.resource, ['extensions', 'recommendations'])) + .then(selection => this.editorService.openEditor({ + resource: workspaceConfigurationFile, + options: { + selection, + forceReload: true // because content has changed + } + })); + } + + protected addExtensionToWorkspaceConfig(workspaceConfigurationFile: URI, extensionId: string, shouldRecommend: boolean) { + return this.getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile) + .then(content => { + const extensionIdLowerCase = extensionId.toLowerCase(); + const workspaceExtensionsConfigContent: IExtensionsConfigContent = (json.parse(content.value) || {})['extensions'] || {}; + let insertInto = shouldRecommend ? workspaceExtensionsConfigContent.recommendations || [] : workspaceExtensionsConfigContent.unwantedRecommendations || []; + let removeFrom = shouldRecommend ? workspaceExtensionsConfigContent.unwantedRecommendations || [] : workspaceExtensionsConfigContent.recommendations || []; + + if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) { + return TPromise.as(null); + } + + insertInto.push(extensionId); + removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase); + + return this.jsonEditingService.write(workspaceConfigurationFile, + { + key: 'extensions', + value: { + recommendations: shouldRecommend ? insertInto : removeFrom, + unwantedRecommendations: shouldRecommend ? removeFrom : insertInto + } + }, + true); + }); + } + + protected addExtensionToWorkspaceFolderConfig(extensionsFileResource: URI, extensionId: string, shouldRecommend: boolean): TPromise { + return this.getOrCreateExtensionsFile(extensionsFileResource) + .then(({ content }) => { + const extensionIdLowerCase = extensionId.toLowerCase(); + const extensionsConfigContent: IExtensionsConfigContent = json.parse(content) || {}; + let insertInto = shouldRecommend ? extensionsConfigContent.recommendations || [] : extensionsConfigContent.unwantedRecommendations || []; + let removeFrom = shouldRecommend ? extensionsConfigContent.unwantedRecommendations || [] : extensionsConfigContent.recommendations || []; + + if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) { + return TPromise.as(null); + } + + insertInto.push(extensionId); + + let removeFromPromise = TPromise.wrap(null); + if (removeFrom.some(e => e.toLowerCase() === extensionIdLowerCase)) { + removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase); + removeFromPromise = this.jsonEditingService.write(extensionsFileResource, + { + key: shouldRecommend ? 'unwantedRecommendations' : 'recommendations', + value: removeFrom + }, + true); + } + + return removeFromPromise.then(() => + this.jsonEditingService.write(extensionsFileResource, + { + key: shouldRecommend ? 'recommendations' : 'unwantedRecommendations', + value: insertInto + }, + true) + ); + }); + } + + protected getWorkspaceExtensionsConfigContent(extensionsFileResource: URI): TPromise { + return this.fileService.resolveContent(extensionsFileResource) + .then(content => { + return (json.parse(content.value) || {})['extensions'] || {}; + }, err => ({ recommendations: [], unwantedRecommendations: [] })); + } + + protected getWorkspaceFolderExtensionsConfigContent(extensionsFileResource: URI): TPromise { + return this.fileService.resolveContent(extensionsFileResource) + .then(content => { + return (json.parse(content.value)); + }, err => ({ recommendations: [], unwantedRecommendations: [] })); + } + + private getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile: URI): TPromise { + return this.fileService.resolveContent(workspaceConfigurationFile) + .then(content => { + const workspaceRecommendations = json.parse(content.value)['extensions']; + if (!workspaceRecommendations || !workspaceRecommendations.recommendations) { + return this.jsonEditingService.write(workspaceConfigurationFile, { key: 'extensions', value: { recommendations: [] } }, true) + .then(() => this.fileService.resolveContent(workspaceConfigurationFile)); + } + return content; + }); + } + + private getSelectionPosition(content: string, resource: URI, path: json.JSONPath): TPromise { + const tree = json.parseTree(content); + const node = json.findNodeAtLocation(tree, path); + if (node && node.parent.children[1]) { + const recommendationsValueNode = node.parent.children[1]; + const lastExtensionNode = recommendationsValueNode.children && recommendationsValueNode.children.length ? recommendationsValueNode.children[recommendationsValueNode.children.length - 1] : null; + const offset = lastExtensionNode ? lastExtensionNode.offset + lastExtensionNode.length : recommendationsValueNode.offset + 1; + return this.textModelResolverService.createModelReference(resource) + .then(reference => { + const position = reference.object.textEditorModel.getPositionAt(offset); + reference.dispose(); + return { + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: position.lineNumber, + endColumn: position.column, + }; + }); + } + return TPromise.as(null); + } + + private getOrCreateExtensionsFile(extensionsFileResource: URI): TPromise<{ created: boolean, extensionsFileResource: URI, content: string }> { + return this.fileService.resolveContent(extensionsFileResource).then(content => { + return { created: false, extensionsFileResource, content: content.value }; + }, err => { + return this.fileService.updateContent(extensionsFileResource, ExtensionsConfigurationInitialContent).then(() => { + return { created: true, extensionsFileResource, content: ExtensionsConfigurationInitialContent }; + }); + }); + } +} + +export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfigureRecommendedExtensionsAction { + + static readonly ID = 'workbench.extensions.action.configureWorkspaceRecommendedExtensions'; + static LABEL = localize('configureWorkspaceRecommendedExtensions', "Configure Recommended Extensions (Workspace)"); + + private disposables: IDisposable[] = []; + + constructor( + id: string, + label: string, + @IFileService fileService: IFileService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IEditorService editorService: IEditorService, + @IJSONEditingService jsonEditingService: IJSONEditingService, + @ITextModelService textModelResolverService: ITextModelService + ) { + super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + this.contextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); + this.update(); + } + + private update(): void { + this.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; + } + + public run(): TPromise { + switch (this.contextService.getWorkbenchState()) { + case WorkbenchState.FOLDER: + return this.openExtensionsFile(this.contextService.getWorkspace().folders[0].toResource(paths.join('.vscode', 'extensions.json'))); + case WorkbenchState.WORKSPACE: + return this.openWorkspaceConfigurationFile(this.contextService.getWorkspace().configuration); + } + return TPromise.as(null); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + +export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends AbstractConfigureRecommendedExtensionsAction { + + static readonly ID = 'workbench.extensions.action.configureWorkspaceFolderRecommendedExtensions'; + static LABEL = localize('configureWorkspaceFolderRecommendedExtensions', "Configure Recommended Extensions (Workspace Folder)"); + + private disposables: IDisposable[] = []; + + constructor( + id: string, + label: string, + @IFileService fileService: IFileService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IEditorService editorService: IEditorService, + @IJSONEditingService jsonEditingService: IJSONEditingService, + @ITextModelService textModelResolverService: ITextModelService, + @ICommandService private commandService: ICommandService + ) { + super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this, this.disposables); + this.update(); + } + + private update(): void { + this.enabled = this.contextService.getWorkspace().folders.length > 0; + } + + public run(): TPromise { + const folderCount = this.contextService.getWorkspace().folders.length; + const pickFolderPromise = folderCount === 1 ? TPromise.as(this.contextService.getWorkspace().folders[0]) : this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); + return pickFolderPromise + .then(workspaceFolder => { + if (workspaceFolder) { + return this.openExtensionsFile(workspaceFolder.toResource(paths.join('.vscode', 'extensions.json'))); + } + return null; + }); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + +export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction { + static readonly ADD = true; + static readonly IGNORE = false; + static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceFolderRecommendations'; + static readonly ADD_LABEL = localize('addToWorkspaceFolderRecommendations', "Add to Recommended Extensions (Workspace Folder)"); + static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations'; + static readonly IGNORE_LABEL = localize('addToWorkspaceFolderIgnoredRecommendations', "Ignore Recommended Extension (Workspace Folder)"); + + constructor( + id: string, + label: string, + @IFileService fileService: IFileService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IEditorService editorService: IEditorService, + @IJSONEditingService jsonEditingService: IJSONEditingService, + @ITextModelService textModelResolverService: ITextModelService, + @ICommandService private commandService: ICommandService, + @INotificationService private notificationService: INotificationService + ) { + super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + } + + run(shouldRecommend: boolean): TPromise { + if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension) { + return TPromise.as(null); + } + const folders = this.contextService.getWorkspace().folders; + if (!folders || !folders.length) { + this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.noWorkspace', 'There are no workspace folders open to add recommendations.')); + return TPromise.as(null); + } + + const extensionId = this.editorService.activeEditor.extension.id; + const pickFolderPromise = folders.length === 1 + ? TPromise.as(folders[0]) + : this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); + return pickFolderPromise + .then(workspaceFolder => { + if (!workspaceFolder) { + return TPromise.as(null); + } + const configurationFile = workspaceFolder.toResource(paths.join('.vscode', 'extensions.json')); + return this.getWorkspaceFolderExtensionsConfigContent(configurationFile).then(content => { + const extensionIdLowerCase = extensionId.toLowerCase(); + if (shouldRecommend) { + if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { + this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s recommendations.')); + return TPromise.as(null); + } + + return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); + }, err => { + this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); + }); + } + else { + if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { + this.notificationService.info(localize('AddToWorkspaceFolderIgnoredRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s unwanted recommendations.')); + return TPromise.as(null); + } + + return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId, shouldRecommend).then(() => { + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openExtensionsFile(configurationFile) + }]); + }, err => { + this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err)); + }); + } + }); + }); + } +} + +export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction { + static readonly ADD = true; + static readonly IGNORE = false; + static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceRecommendations'; + static readonly ADD_LABEL = localize('addToWorkspaceRecommendations', "Add to Recommended Extensions (Workspace)"); + static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations'; + static readonly IGNORE_LABEL = localize('addToWorkspaceIgnoredRecommendations', "Ignore Recommended Extension (Workspace)"); + + constructor( + id: string, + label: string, + @IFileService fileService: IFileService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IEditorService editorService: IEditorService, + @IJSONEditingService jsonEditingService: IJSONEditingService, + @ITextModelService textModelResolverService: ITextModelService, + @INotificationService private notificationService: INotificationService + ) { + super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + } + + run(shouldRecommend: boolean): TPromise { + if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension) { + return TPromise.as(null); + } + const workspaceConfig = this.contextService.getWorkspace().configuration; + + const extensionId = this.editorService.activeEditor.extension.id; + + return this.getWorkspaceExtensionsConfigContent(workspaceConfig).then(content => { + const extensionIdLowerCase = extensionId.toLowerCase(); + if (shouldRecommend) { + if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { + this.notificationService.info(localize('AddToWorkspaceRecommendations.alreadyExists', 'This extension is already present in workspace recommendations.')); + return TPromise.as(null); + } + + return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); + + }, err => { + this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); + }); + } else { + if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) { + this.notificationService.info(localize('AddToWorkspaceUnwantedRecommendations.alreadyExists', 'This extension is already present in workspace unwanted recommendations.')); + return TPromise.as(null); + } + + return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId, shouldRecommend).then(() => { + this.notificationService.prompt(Severity.Info, + localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.'), + [{ + label: localize('viewChanges', "View Changes"), + run: () => this.openWorkspaceConfigurationFile(workspaceConfig) + }]); + }, err => { + this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err)); + }); + } + }); + } +} + +export class MaliciousStatusLabelAction extends Action { + + private static readonly Class = 'malicious-status'; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(long: boolean) { + const tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); + const label = long ? tooltip : localize('malicious', "Malicious"); + super('extensions.install', label, '', false); + this.tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); + } + + private update(): void { + if (this.extension && this.extension.isMalicious) { + this.class = `${MaliciousStatusLabelAction.Class} malicious`; + } else { + this.class = `${MaliciousStatusLabelAction.Class} not-malicious`; + } + } + + run(): TPromise { + return TPromise.as(null); + } +} + +export class DisabledStatusLabelAction extends Action { + + private static readonly Class = 'disable-status'; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + private disposables: IDisposable[] = []; + private throttler: Throttler = new Throttler(); + + constructor( + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionService private extensionService: IExtensionService + ) { + super('extensions.install', localize('disabled', "Disabled"), `${DisabledStatusLabelAction.Class} hide`, false); + this.disposables.push(this.extensionsWorkbenchService.onChange((extension) => this.update(extension))); + this.update(); + } + + private update(extension?: IExtension): void { + if (extension && this.extension && !areSameExtensions(this.extension, extension)) { + return; + } + this.throttler.queue(() => this.extensionService.getExtensions() + .then(runningExtensions => { + this.class = `${DisabledStatusLabelAction.Class} hide`; + this.tooltip = ''; + if (this.extension && !this.extension.isMalicious && !runningExtensions.some(e => e.id === this.extension.id)) { + if (this.extension.enablementState === EnablementState.Disabled || this.extension.enablementState === EnablementState.WorkspaceDisabled) { + this.class = `${DisabledStatusLabelAction.Class}`; + this.tooltip = this.extension.enablementState === EnablementState.Disabled ? localize('disabled globally', "Disabled") : localize('disabled workspace', "Disabled for this Workspace"); + } + } + })); + } + + run(): TPromise { + return TPromise.as(null); + } +} + +export class DisableAllAction extends Action { + + static readonly ID = 'workbench.extensions.action.disableAll'; + static LABEL = localize('disableAll', "Disable All Installed Extensions"); + + private disposables: IDisposable[] = []; + + constructor( + id: string = DisableAllAction.ID, label: string = DisableAllAction.LABEL, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(id, label); + this.update(); + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + } + + private update(): void { + this.enabled = this.extensionsWorkbenchService.local.some(e => e.type === LocalExtensionType.User && (e.enablementState === EnablementState.Enabled || e.enablementState === EnablementState.WorkspaceEnabled)); + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local.filter(e => e.type === LocalExtensionType.User), EnablementState.Disabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class DisableAllWorkpsaceAction extends Action { + + static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; + static LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); + + private disposables: IDisposable[] = []; + + constructor( + id: string = DisableAllWorkpsaceAction.ID, label: string = DisableAllWorkpsaceAction.LABEL, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(id, label); + this.update(); + this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); + this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); + } + + private update(): void { + this.enabled = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.extensionsWorkbenchService.local.some(e => e.type === LocalExtensionType.User && (e.enablementState === EnablementState.Enabled || e.enablementState === EnablementState.WorkspaceEnabled)); + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local.filter(e => e.type === LocalExtensionType.User), EnablementState.WorkspaceDisabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class EnableAllAction extends Action { + + static readonly ID = 'workbench.extensions.action.enableAll'; + static LABEL = localize('enableAll', "Enable All Extensions"); + + private disposables: IDisposable[] = []; + + constructor( + id: string = EnableAllAction.ID, label: string = EnableAllAction.LABEL, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(id, label); + this.update(); + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + } + + private update(): void { + this.enabled = this.extensionsWorkbenchService.local.some(e => e.local && this.extensionEnablementService.canChangeEnablement(e.local) && (e.enablementState === EnablementState.Disabled || e.enablementState === EnablementState.WorkspaceDisabled)); + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local, EnablementState.Enabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} + +export class EnableAllWorkpsaceAction extends Action { + + static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; + static LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); + + private disposables: IDisposable[] = []; + + constructor( + id: string = EnableAllWorkpsaceAction.ID, label: string = EnableAllWorkpsaceAction.LABEL, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + super(id, label); + this.update(); + this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables); + this.workspaceContextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); + } + + private update(): void { + this.enabled = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.extensionsWorkbenchService.local.some(e => e.local && this.extensionEnablementService.canChangeEnablement(e.local) && (e.enablementState === EnablementState.Disabled || e.enablementState === EnablementState.WorkspaceDisabled)); + } + + run(): TPromise { + return this.extensionsWorkbenchService.setEnablement(this.extensionsWorkbenchService.local, EnablementState.WorkspaceEnabled); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} export class OpenExtensionsFolderAction extends Action { @@ -97,7 +2303,7 @@ export class ReinstallAction extends Action { constructor( id: string = ReinstallAction.ID, label: string = ReinstallAction.LABEL, @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @INotificationService private notificationService: INotificationService, @IWindowService private windowService: IWindowService ) { @@ -109,21 +2315,22 @@ export class ReinstallAction extends Action { } run(): TPromise { - return this.quickOpenService.pick(this.getEntries(), { placeHolder: localize('selectExtension', "Select Extension to Reinstall") }); + return this.quickInputService.pick(this.getEntries(), { placeHolder: localize('selectExtension', "Select Extension to Reinstall") }) + .then(pick => pick && this.reinstallExtension(pick.extension)); } - private getEntries(): TPromise { + private getEntries() { return this.extensionsWorkbenchService.queryLocal() .then(local => { - const entries: IPickOpenEntry[] = local + const entries = local .filter(extension => extension.type === LocalExtensionType.User) .map(extension => { - return { + return { id: extension.id, label: extension.displayName, description: extension.id, - run: () => this.reinstallExtension(extension), - }; + extension, + } as (IQuickPickItem & { extension: IExtension }); }); return entries; }); @@ -142,4 +2349,97 @@ export class ReinstallAction extends Action { ); }, error => this.notificationService.error(error)); } -} \ No newline at end of file +} + +CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForLanguage', function (accessor: ServicesAccessor, fileExtension: string) { + const viewletService = accessor.get(IViewletService); + + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(`ext:${fileExtension.replace(/^\./, '')}`); + viewlet.focus(); + }); +}); + +CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithIds', function (accessor: ServicesAccessor, extensionIds: string[]) { + const viewletService = accessor.get(IViewletService); + + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + const query = extensionIds + .map(id => `@id:${id}`) + .join(' '); + viewlet.search(query); + viewlet.focus(); + }); +}); + +export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { + dark: '#327e36', + light: '#327e36', + hc: null +}, localize('extensionButtonProminentBackground', "Button background color for actions extension that stand out (e.g. install button).")); + +export const extensionButtonProminentForeground = registerColor('extensionButton.prominentForeground', { + dark: Color.white, + light: Color.white, + hc: null +}, localize('extensionButtonProminentForeground', "Button foreground color for actions extension that stand out (e.g. install button).")); + +export const extensionButtonProminentHoverBackground = registerColor('extensionButton.prominentHoverBackground', { + dark: '#28632b', + light: '#28632b', + hc: null +}, localize('extensionButtonProminentHoverBackground', "Button background hover color for actions extension that stand out (e.g. install button).")); + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); + } + + const buttonBackgroundColor = theme.getColor(buttonBackground); + if (buttonBackgroundColor) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { background-color: ${buttonBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { background-color: ${buttonBackgroundColor}; }`); + } + + const buttonForegroundColor = theme.getColor(buttonForeground); + if (buttonForegroundColor) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { color: ${buttonForegroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { color: ${buttonForegroundColor}; }`); + } + + const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground); + if (buttonHoverBackgroundColor) { + collector.addRule(`.extension .monaco-action-bar .action-item:hover .action-label.extension-action { background-color: ${buttonHoverBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action { background-color: ${buttonHoverBackgroundColor}; }`); + } + + const contrastBorderColor = theme.getColor(contrastBorder); + if (contrastBorderColor) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); + } + + const extensionButtonProminentBackgroundColor = theme.getColor(extensionButtonProminentBackground); + if (extensionButtonProminentBackground) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); + } + + const extensionButtonProminentForegroundColor = theme.getColor(extensionButtonProminentForeground); + if (extensionButtonProminentForeground) { + collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.prominent { color: ${extensionButtonProminentForegroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.prominent { color: ${extensionButtonProminentForegroundColor}; }`); + } + + const extensionButtonProminentHoverBackgroundColor = theme.getColor(extensionButtonProminentHoverBackground); + if (extensionButtonProminentHoverBackground) { + collector.addRule(`.extension .monaco-action-bar .action-item:hover .action-label.extension-action.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + } +}); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts new file mode 100644 index 00000000000..b41009aea78 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { localize } from 'vs/nls'; +import { append, $, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Action } from 'vs/base/common/actions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; +import { once } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, extensionButtonProminentBackground, extensionButtonProminentForeground, MaliciousStatusLabelAction, DisabledStatusLabelAction } 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 { 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 { INotificationService } from 'vs/platform/notification/common/notification'; +import { createCancelablePromise } from 'vs/base/common/async'; + +export interface ITemplateData { + root: HTMLElement; + element: HTMLElement; + icon: HTMLImageElement; + name: HTMLElement; + installCount: HTMLElement; + ratings: HTMLElement; + author: HTMLElement; + description: HTMLElement; + extension: IExtension; + disposables: IDisposable[]; + extensionDisposables: IDisposable[]; +} + +export class Delegate implements IVirtualDelegate { + getHeight() { return 62; } + getTemplateId() { return 'extension'; } +} + +const actionOptions = { icon: true, label: true }; + +export class Renderer implements IPagedRenderer { + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @INotificationService private notificationService: INotificationService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionService private extensionService: IExtensionService, + @IExtensionTipsService private extensionTipsService: IExtensionTipsService, + @IThemeService private themeService: IThemeService, + @IExtensionManagementServerService private 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 element = append(root, $('.extension')); + const icon = append(element, $('img.icon')); + const details = append(element, $('.details')); + const headerContainer = append(details, $('.header-container')); + const header = append(headerContainer, $('.header')); + const name = append(header, $('span.name')); + const version = append(header, $('span.version')); + const installCount = append(header, $('span.install-count')); + const ratings = append(header, $('span.ratings')); + const description = append(details, $('.description.ellipsis')); + const footer = append(details, $('.footer')); + const author = append(footer, $('.author.ellipsis')); + const actionbar = new ActionBar(footer, { + animated: false, + actionItemProvider: (action: Action) => { + if (action.id === ManageExtensionAction.ID) { + return (action).actionItem; + } + return null; + } + }); + actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); + + const versionWidget = this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version); + const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, { small: true }); + const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratings, { small: true }); + + const maliciousStatusAction = this.instantiationService.createInstance(MaliciousStatusLabelAction, false); + const disabledStatusAction = this.instantiationService.createInstance(DisabledStatusLabelAction); + const installAction = this.instantiationService.createInstance(InstallAction); + const updateAction = this.instantiationService.createInstance(UpdateAction); + const reloadAction = this.instantiationService.createInstance(ReloadAction); + const manageAction = this.instantiationService.createInstance(ManageExtensionAction); + + actionbar.push([updateAction, reloadAction, installAction, disabledStatusAction, maliciousStatusAction, manageAction], actionOptions); + const disposables = [versionWidget, installCountWidget, ratingsWidget, maliciousStatusAction, disabledStatusAction, updateAction, installAction, reloadAction, manageAction, actionbar, bookmarkStyler]; + + return { + root, element, icon, name, installCount, ratings, author, description, disposables, + extensionDisposables: [], + set extension(extension: IExtension) { + versionWidget.extension = extension; + installCountWidget.extension = extension; + ratingsWidget.extension = extension; + maliciousStatusAction.extension = extension; + disabledStatusAction.extension = extension; + installAction.extension = extension; + updateAction.extension = extension; + reloadAction.extension = extension; + manageAction.extension = extension; + } + }; + } + + renderPlaceholder(index: number, data: ITemplateData): void { + addClass(data.element, 'loading'); + + data.root.removeAttribute('aria-label'); + data.extensionDisposables = dispose(data.extensionDisposables); + data.icon.src = ''; + data.name.textContent = ''; + data.author.textContent = ''; + data.description.textContent = ''; + data.installCount.style.display = 'none'; + data.ratings.style.display = 'none'; + data.extension = null; + } + + renderElement(extension: IExtension, index: number, data: ITemplateData): void { + removeClass(data.element, 'loading'); + + data.extensionDisposables = dispose(data.extensionDisposables); + const installed = this.extensionsWorkbenchService.local.filter(e => e.id === extension.id)[0]; + + this.extensionService.getExtensions().then(runningExtensions => { + if (installed && installed.local) { + const installedExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(installed.local.location); + const isSameExtensionRunning = runningExtensions.some(e => areSameExtensions(e, extension) && installedExtensionServer.authority === this.extensionManagementServerService.getExtensionManagementServer(e.extensionLocation).authority); + toggleClass(data.root, 'disabled', !isSameExtensionRunning); + } else { + removeClass(data.root, 'disabled'); + } + }); + + const onError = once(domEvent(data.icon, 'error')); + onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); + data.icon.src = extension.iconUrl; + + if (!data.icon.complete) { + data.icon.style.visibility = 'hidden'; + data.icon.onload = () => data.icon.style.visibility = 'inherit'; + } else { + data.icon.style.visibility = 'inherit'; + } + + this.updateRecommendationStatus(extension, data); + data.extensionDisposables.push(this.extensionTipsService.onRecommendationChange(change => { + if (change.extensionId.toLowerCase() === extension.id.toLowerCase()) { + this.updateRecommendationStatus(extension, data); + } + })); + + data.name.textContent = extension.displayName; + data.author.textContent = extension.publisherDisplayName; + data.description.textContent = extension.description; + data.installCount.style.display = ''; + data.ratings.style.display = ''; + data.extension = extension; + + const manifestPromise = createCancelablePromise(token => extension.getManifest(token).then(manifest => { + if (manifest) { + const name = manifest && manifest.contributes && manifest.contributes.localizations && manifest.contributes.localizations.length > 0 && manifest.contributes.localizations[0].localizedLanguageName; + if (name) { data.description.textContent = name[0].toLocaleUpperCase() + name.slice(1); } + } + })); + data.disposables.push(toDisposable(() => manifestPromise.cancel())); + } + + disposeElement(): void { + // noop + } + + private updateRecommendationStatus(extension: IExtension, data: ITemplateData) { + const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); + let ariaLabel = extension.displayName + '. '; + + if (!extRecommendations[extension.id.toLowerCase()]) { + removeClass(data.root, 'recommended'); + data.root.title = ''; + } else { + addClass(data.root, 'recommended'); + ariaLabel += extRecommendations[extension.id.toLowerCase()].reasonText + ' '; + data.root.title = extRecommendations[extension.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/extensionsUtils.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts index 104644f1c7f..edb456d35f0 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsUtils.ts @@ -16,7 +16,7 @@ import { IExtensionManagementService, ILocalExtension, IExtensionEnablementServi import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { areSameExtensions, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; @@ -136,7 +136,7 @@ export function getInstalledExtensions(accessor: ServicesAccessor): TPromise areSameExtensions({ id: extensionId }, { id: getGalleryExtensionIdFromLocal(extension.local) })); } function stripVersion(id: string): string { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index ab983addc7c..4a36c1bee2e 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -7,42 +7,38 @@ import 'vs/css!./media/extensionsViewlet'; import { localize } from 'vs/nls'; -import { ThrottledDelayer, always } from 'vs/base/common/async'; +import { ThrottledDelayer, always, timeout } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; -import { isPromiseCanceledError, onUnexpectedError, create as createError } from 'vs/base/common/errors'; +import { isPromiseCanceledError, create as createError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event as EventOf, mapEvent, chain } from 'vs/base/common/event'; +import { Event as EventOf, Emitter } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; -import { domEvent } from 'vs/base/browser/event'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionState, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, VIEW_CONTAINER } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionState, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, CloseExtensionDetailsOnViewChangeKey, VIEW_CONTAINER } from '../common/extensions'; import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction -} from 'vs/workbench/parts/extensions/browser/extensionsActions'; -import { LocalExtensionType, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, ChangeGroupAction +} from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; +import { LocalExtensionType, IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; -import { ExtensionsListView, InstalledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView } from './extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, GroupByServerExtensionsView, DefaultRecommendedExtensionsView } from './extensionsViews'; import { OpenGlobalSettingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { inputForeground, inputBackground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views'; -import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { ViewContainerViewlet, IViewletViewOptions } 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'; @@ -54,6 +50,12 @@ 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 { SingleServerExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; +import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/parts/codeEditor/browser/suggestEnabledInput'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -62,14 +64,16 @@ interface SearchInputEvent extends Event { const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const SearchExtensionsContext = new RawContextKey('searchExtensions', false); -const SearchInstalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); +const HasInstalledExtensionsContext = new RawContextKey('hasInstalledExtensions', true); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); +const GroupByServersContext = new RawContextKey('groupByServersContext', false); export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { constructor( + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService ) { this.registerViews(); } @@ -77,14 +81,22 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio private registerViews(): void { let viewDescriptors = []; viewDescriptors.push(this.createMarketPlaceExtensionsListViewDescriptor()); - viewDescriptors.push(this.createInstalledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createSearchInstalledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createSearchBuiltInExtensionsListViewDescriptor()); - viewDescriptors.push(this.createSearchBuiltInBasicsExtensionsListViewDescriptor()); - viewDescriptors.push(this.createSearchBuiltInThemesExtensionsListViewDescriptor()); + viewDescriptors.push(this.createEnabledExtensionsListViewDescriptor()); + viewDescriptors.push(this.createDisabledExtensionsListViewDescriptor()); + viewDescriptors.push(this.createPopularExtensionsListViewDescriptor()); + viewDescriptors.push(this.createBuiltInExtensionsListViewDescriptor()); + viewDescriptors.push(this.createBuiltInBasicsExtensionsListViewDescriptor()); + viewDescriptors.push(this.createBuiltInThemesExtensionsListViewDescriptor()); viewDescriptors.push(this.createDefaultRecommendedExtensionsListViewDescriptor()); viewDescriptors.push(this.createOtherRecommendedExtensionsListViewDescriptor()); viewDescriptors.push(this.createWorkspaceRecommendedExtensionsListViewDescriptor()); + + if (this.extensionManagementServerService.extensionManagementServers.length > 1) { + for (const extensionManagementServer of this.extensionManagementServerService.extensionManagementServers) { + viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(extensionManagementServer)); + } + } + ViewsRegistry.registerViews(viewDescriptors); } @@ -94,42 +106,69 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: localize('marketPlace', "Marketplace"), container: VIEW_CONTAINER, ctor: ExtensionsListView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('searchExtensions'), ContextKeyExpr.not('searchInstalledExtensions'), ContextKeyExpr.not('searchBuiltInExtensions'), ContextKeyExpr.not('recommendedExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchExtensions'), ContextKeyExpr.not('searchInstalledExtensions'), ContextKeyExpr.not('searchBuiltInExtensions'), ContextKeyExpr.not('recommendedExtensions'), ContextKeyExpr.not('groupByServersContext')), weight: 100 }; } - private createInstalledExtensionsListViewDescriptor(): IViewDescriptor { + private createEnabledExtensionsListViewDescriptor(): IViewDescriptor { return { - id: 'extensions.installedList', - name: localize('installedExtensions', "Installed"), + id: 'extensions.enabledExtensionList', + name: localize('enabledExtensions', "Enabled"), container: VIEW_CONTAINER, - ctor: InstalledExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.not('searchExtensions')), - order: 1, - weight: 30 + ctor: EnabledExtensionsView, + when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('hasInstalledExtensions')), + weight: 40, + canToggleVisibility: true, + order: 1 }; } - private createSearchInstalledExtensionsListViewDescriptor(): IViewDescriptor { + private createDisabledExtensionsListViewDescriptor(): IViewDescriptor { return { - id: 'extensions.searchInstalledList', - name: localize('searchInstalledExtensions', "Installed"), + id: 'extensions.disabledExtensionList', + name: localize('disabledExtensions', "Disabled"), container: VIEW_CONTAINER, - ctor: InstalledExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('searchInstalledExtensions')), - weight: 100 + ctor: DisabledExtensionsView, + when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('hasInstalledExtensions')), + weight: 10, + canToggleVisibility: true, + order: 3, + collapsed: true }; } + private createPopularExtensionsListViewDescriptor(): IViewDescriptor { + return { + id: 'extensions.popularExtensionsList', + name: localize('popularExtensions', "Popular"), + container: VIEW_CONTAINER, + ctor: ExtensionsListView, + when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.not('hasInstalledExtensions')), + weight: 60, + order: 1 + }; + } + + private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { + return [{ + id: `server.extensionsList.${server.authority}`, + name: server.label, + container: VIEW_CONTAINER, + ctor: GroupByServerExtensionsView, + when: ContextKeyExpr.has('groupByServersContext'), + weight: 100 + }]; + } + private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor { return { id: 'extensions.recommendedList', name: localize('recommendedExtensions', "Recommended"), container: VIEW_CONTAINER, - ctor: RecommendedExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')), - weight: 70, + ctor: DefaultRecommendedExtensionsView, + when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')), + weight: 40, order: 2, canToggleVisibility: true }; @@ -141,7 +180,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: localize('otherRecommendedExtensions', "Other Recommendations"), container: VIEW_CONTAINER, ctor: RecommendedExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('recommendedExtensions')), + when: ContextKeyExpr.has('recommendedExtensions'), weight: 50, canToggleVisibility: true, order: 2 @@ -154,44 +193,44 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), container: VIEW_CONTAINER, ctor: WorkspaceRecommendedExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), + when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), weight: 50, canToggleVisibility: true, order: 1 }; } - private createSearchBuiltInExtensionsListViewDescriptor(): IViewDescriptor { + private createBuiltInExtensionsListViewDescriptor(): IViewDescriptor { return { id: 'extensions.builtInExtensionsList', name: localize('builtInExtensions', "Features"), container: VIEW_CONTAINER, ctor: BuiltInExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('searchBuiltInExtensions')), + when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100, canToggleVisibility: true }; } - private createSearchBuiltInThemesExtensionsListViewDescriptor(): IViewDescriptor { + private createBuiltInThemesExtensionsListViewDescriptor(): IViewDescriptor { return { id: 'extensions.builtInThemesExtensionsList', name: localize('builtInThemesExtensions', "Themes"), container: VIEW_CONTAINER, ctor: BuiltInThemesExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('searchBuiltInExtensions')), + when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100, canToggleVisibility: true }; } - private createSearchBuiltInBasicsExtensionsListViewDescriptor(): IViewDescriptor { + private createBuiltInBasicsExtensionsListViewDescriptor(): IViewDescriptor { return { id: 'extensions.builtInBasicsExtensionsList', name: localize('builtInBasicsExtensions', "Programming Languages"), container: VIEW_CONTAINER, ctor: BuiltInBasicsExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('donotshowExtensions'), ContextKeyExpr.has('searchBuiltInExtensions')), + when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100, canToggleVisibility: true }; @@ -203,18 +242,20 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private onSearchChange: EventOf; private nonEmptyWorkspaceContextKey: IContextKey; private searchExtensionsContextKey: IContextKey; - private searchInstalledExtensionsContextKey: IContextKey; + private hasInstalledExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; + private groupByServersContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: ThrottledDelayer; private root: HTMLElement; - private searchBox: HTMLInputElement; + private searchBox: SuggestEnabledInput; private extensionsBox: HTMLElement; private primaryActions: IAction[]; private secondaryActions: IAction[]; + private groupByServerAction: IAction; private disposables: IDisposable[] = []; constructor( @@ -232,20 +273,26 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IWorkspaceContextService contextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService ) { super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); this.searchDelayer = new ThrottledDelayer(500); this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); - this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); + this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService); this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); + this.groupByServersContextKey = GroupByServersContext.bindTo(contextKeyService); this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables)); + this.extensionManagementService.getInstalled(LocalExtensionType.User).then(result => { + this.hasInstalledExtensionsContextKey.set(result.length > 0); + }); + this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { this.secondaryActions = null; @@ -257,51 +304,40 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio }, this, this.disposables); } - async create(parent: HTMLElement): TPromise { + create(parent: HTMLElement): TPromise { addClass(parent, 'extensions-viewlet'); this.root = parent; const header = append(this.root, $('.header')); - this.searchBox = append(header, $('input.search-box')); - this.searchBox.placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); - this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.FOCUS, () => addClass(this.searchBox, 'synthetic-focus'))); - this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.BLUR, () => removeClass(this.searchBox, 'synthetic-focus'))); + const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); + + this.searchBox = this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, header, { + triggerCharacters: ['@'], + sortKey: item => { + if (item.indexOf(':') === -1) { return 'a'; } + else if (/ext:/.test(item) || /tag:/.test(item)) { return 'b'; } + else if (/sort:/.test(item)) { return 'c'; } + else { return 'd'; } + }, + provideResults: (query) => Query.suggestions(query) + }, placeholder, 'extensions:searchinput', { placeholderText: placeholder }); + + this.disposables.push(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); + + this.disposables.push(this.searchBox); + + const _searchChange = new Emitter(); + this.onSearchChange = _searchChange.event; + this.searchBox.onInputDidChange(() => { + this.triggerSearch(); + _searchChange.fire(this.searchBox.getValue()); + }, this, this.disposables); + + this.searchBox.onShouldFocusResults(() => this.focusListView(), this, this.disposables); this.extensionsBox = append(this.root, $('.extensions')); - - const onKeyDown = chain(domEvent(this.searchBox, 'keydown')) - .map(e => new StandardKeyboardEvent(e)); - onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables); - - const onKeyDownForList = onKeyDown.filter(() => this.count() > 0); - onKeyDownForList.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables); - - const onSearchInput = domEvent(this.searchBox, 'input') as EventOf; - onSearchInput(e => this.triggerSearch(e.immediate), null, this.disposables); - - this.onSearchChange = mapEvent(onSearchInput, e => e.target.value); - - await super.create(this.extensionsBox); - - const installed = await this.extensionManagementService.getInstalled(LocalExtensionType.User); - - if (installed.length === 0) { - this.searchBox.value = '@sort:installs'; - this.searchExtensionsContextKey.set(true); - } - } - - public updateStyles(): void { - super.updateStyles(); - - this.searchBox.style.backgroundColor = this.getColor(inputBackground); - this.searchBox.style.color = this.getColor(inputForeground); - - const inputBorderColor = this.getColor(inputBorder); - this.searchBox.style.borderWidth = inputBorderColor ? '1px' : null; - this.searchBox.style.borderStyle = inputBorderColor ? 'solid' : null; - this.searchBox.style.borderColor = inputBorderColor; + return super.create(this.extensionsBox); } setVisible(visible: boolean): TPromise { @@ -310,7 +346,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio if (isVisibilityChanged) { if (visible) { this.searchBox.focus(); - this.searchBox.setSelectionRange(0, this.searchBox.value.length); } } }); @@ -322,6 +357,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio layout(dimension: Dimension): void { toggleClass(this.root, 'narrow', dimension.width <= 300); + this.searchBox.layout({ height: 20, width: dimension.width - 34 }); super.layout(new Dimension(dimension.width, dimension.height - 38)); } @@ -340,6 +376,12 @@ 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), @@ -353,6 +395,7 @@ 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.extensionManagementServers.length > 1 ? [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), @@ -369,44 +412,59 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio const event = new Event('input', { bubbles: true }) as SearchInputEvent; event.immediate = true; - this.searchBox.value = value; - this.searchBox.dispatchEvent(event); + this.searchBox.setValue(value); } private triggerSearch(immediate = false): void { - this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.value ? 0 : 500) - .done(null, err => this.onError(err)); + this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.getValue() ? 0 : 500).then(null, err => this.onError(err)); } - private async doSearch(): TPromise { - const value = this.searchBox.value || ''; + private normalizedQuery(): string { + return this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); + } + + private doSearch(): TPromise { + const value = this.normalizedQuery(); this.searchExtensionsContextKey.set(!!value); - this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInstalledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); + this.groupByServersContextKey.set(ExtensionsListView.isGroupByServersExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(ExtensionsListView.isRecommendedExtensionsQuery(value)); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); if (value) { - this.progress(TPromise.join(this.panels.map(view => (view).show(this.searchBox.value)))); + return this.progress(TPromise.join(this.panels.map(view => (view).show(this.normalizedQuery())))); } + return TPromise.as(null); } protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { const addedViews = super.onDidAddViews(added); - this.progress(TPromise.join(addedViews.map(addedView => (addedView).show(this.searchBox.value)))); + this.progress(TPromise.join(addedViews.map(addedView => (addedView).show(this.normalizedQuery())))); return addedViews; } + protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel { + for (const extensionManagementServer of this.extensionManagementServerService.extensionManagementServers) { + if (viewDescriptor.id === `server.extensionsList.${extensionManagementServer.authority}`) { + const servicesCollection: ServiceCollection = new ServiceCollection(); + servicesCollection.set(IExtensionManagementServerService, new SingleServerExtensionManagementServerService(extensionManagementServer)); + 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); } - private onEscape(): void { - this.search(''); - } - - private onEnter(): void { - (this.panels[0]).select(); + private focusListView(): void { + if (this.count() > 0) { + this.panels[0].focus(); + } } private onViewletOpen(viewlet: IViewlet): void { @@ -414,14 +472,16 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return; } - const promises = this.editorGroupService.groups.map(group => { - const editors = group.editors.filter(input => input instanceof ExtensionsInput); - const promises = editors.map(editor => group.closeEditor(editor)); + if (this.configurationService.getValue(CloseExtensionDetailsOnViewChangeKey)) { + const promises = this.editorGroupService.groups.map(group => { + const editors = group.editors.filter(input => input instanceof ExtensionsInput); + const promises = editors.map(editor => group.closeEditor(editor)); - return TPromise.join(promises); - }); + return TPromise.join(promises); + }); - TPromise.join(promises).done(null, onUnexpectedError); + TPromise.join(promises); + } } private progress(promise: TPromise): TPromise { @@ -505,7 +565,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { private loopCheckForMaliciousExtensions(): void { this.checkForMaliciousExtensions() - .then(() => TPromise.timeout(1000 * 60 * 5)) // every five minutes + .then(() => timeout(1000 * 60 * 5)) // every five minutes .then(() => this.loopCheckForMaliciousExtensions()); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index 9e74a3c633c..4e4446bc143 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -5,20 +5,21 @@ 'use strict'; + import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { dispose } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { chain } from 'vs/base/common/event'; import { isPromiseCanceledError, create as createError } from 'vs/base/common/errors'; -import { PagedModel, IPagedModel, IPager } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions, LocalExtensionType, IExtensionTipsService, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; +import { SortBy, SortOrder, IQueryOptions, LocalExtensionType, IExtensionTipsService, EnablementState, IExtensionRecommendation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { append, $, toggleClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Delegate, Renderer } from 'vs/workbench/parts/extensions/browser/extensionsList'; +import { Delegate, Renderer } from 'vs/workbench/parts/extensions/electron-browser/extensionsList'; import { IExtension, IExtensionsWorkbenchService } from '../common/extensions'; import { Query } from '../common/extensionQuery'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -31,11 +32,14 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; +import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { distinct } from 'vs/base/common/arrays'; +import { IExperimentService } from 'vs/workbench/parts/experiments/node/experimentService'; export class ExtensionsListView extends ViewletPanel { @@ -53,19 +57,24 @@ export class ExtensionsListView extends ViewletPanel { @IInstantiationService protected instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, @IExtensionService private extensionService: IExtensionService, - @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, @IEditorService private editorService: IEditorService, - @IExtensionTipsService private tipsService: IExtensionTipsService, + @IExtensionTipsService protected tipsService: IExtensionTipsService, @IModeService private modeService: IModeService, @ITelemetryService private telemetryService: ITelemetryService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService protected contextService: IWorkspaceContextService, + @IExperimentService private experimentService: IExperimentService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); } - renderHeader(container: HTMLElement): void { - const titleDiv = append(container, $('div.title')); - append(titleDiv, $('span')).textContent = this.options.title; + protected renderHeader(container: HTMLElement): void { + this.renderHeaderTitle(container); + } + + renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.options.title); this.badgeContainer = append(container, $('.count-badge-wrapper')); this.badge = new CountBadge(this.badgeContainer); @@ -78,8 +87,10 @@ export class ExtensionsListView extends ViewletPanel { const delegate = new Delegate(); const renderer = this.instantiationService.createInstance(Renderer); this.list = this.instantiationService.createInstance(WorkbenchPagedList, this.extensionsList, delegate, [renderer], { - ariaLabel: localize('extensions', "Extensions") + ariaLabel: localize('extensions', "Extensions"), + multipleSelectionSupport: false }) as WorkbenchPagedList; + this.disposables.push(this.list); chain(this.list.onOpen) .map(e => e.elements[0]) @@ -97,10 +108,16 @@ export class ExtensionsListView extends ViewletPanel { this.list.layout(size); } - async show(query: string): TPromise> { - const model = await this.query(query); - this.setModel(model); - return model; + async show(query: string): Promise> { + return await this.query(query).then(model => { + this.setModel(model); + return model; + }).catch(e => { + console.warn('Error querying extensions gallery', e); + const model = new PagedModel([]); + this.setModel(model, true); + return model; + }); } select(): void { @@ -131,7 +148,13 @@ export class ExtensionsListView extends ViewletPanel { return this.list.length; } - private async query(value: string): TPromise> { + protected showEmptyModel(): TPromise> { + const emptyModel = new PagedModel([]); + this.setModel(emptyModel); + return TPromise.as(emptyModel); + } + + private async query(value: string): Promise> { const query = Query.parse(value); let options: IQueryOptions = { @@ -143,6 +166,9 @@ export class ExtensionsListView extends ViewletPanel { case 'rating': options = assign(options, { sortBy: SortBy.WeightedRating }); break; case 'name': options = assign(options, { sortBy: SortBy.Title }); break; } + if (!value || !value.trim()) { + options.sortBy = SortBy.InstallCount; + } if (/@builtin/i.test(value)) { const showThemesOnly = /@builtin:themes/i.test(value); @@ -177,8 +203,8 @@ export class ExtensionsListView extends ViewletPanel { const basics = result.filter(e => { return e.local.manifest && e.local.manifest.contributes - && Array.isArray(e.local.manifest.contributes.languages) - && e.local.manifest.contributes.languages.length + && Array.isArray(e.local.manifest.contributes.grammars) + && e.local.manifest.contributes.grammars.length && e.local.identifier.id !== 'git'; }); return new PagedModel(this.sortExtensions(basics, options)); @@ -187,7 +213,7 @@ export class ExtensionsListView extends ViewletPanel { const others = result.filter(e => { return e.local.manifest && e.local.manifest.contributes - && (!Array.isArray(e.local.manifest.contributes.languages) || e.local.identifier.id === 'git') + && (!Array.isArray(e.local.manifest.contributes.grammars) || e.local.identifier.id === 'git') && !Array.isArray(e.local.manifest.contributes.themes); }); return new PagedModel(this.sortExtensions(others, options)); @@ -196,9 +222,9 @@ export class ExtensionsListView extends ViewletPanel { return new PagedModel(this.sortExtensions(result, options)); } - if (!value || ExtensionsListView.isInstalledExtensionsQuery(value)) { + if (/@installed/i.test(value)) { // Show installed extensions - value = value ? value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : ''; + value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); let result = await this.extensionsWorkbenchService.queryLocal(); @@ -208,12 +234,16 @@ export class ExtensionsListView extends ViewletPanel { return new PagedModel(this.sortExtensions(result, options)); } - const idMatch = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/.exec(value); - - if (idMatch) { + const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; + let idMatch; + const names: string[] = []; + while ((idMatch = idRegex.exec(value)) !== null) { const name = idMatch[1]; + names.push(name); + } - return this.extensionsWorkbenchService.queryGallery({ names: [name], source: 'queryById' }) + if (names.length) { + return this.extensionsWorkbenchService.queryGallery({ names, source: 'queryById' }) .then(pager => new PagedModel(pager)); } @@ -266,6 +296,10 @@ export class ExtensionsListView extends ViewletPanel { return this.getRecommendationsModel(query, options); } + if (/\bcurated:([^\s]+)\b/.test(query.value)) { + return this.getCuratedModel(query, options); + } + let text = query.value; const extensionRegex = /\bext:([^\s]+)\b/g; @@ -286,8 +320,7 @@ export class ExtensionsListView extends ViewletPanel { if (text !== query.value) { options = assign(options, { text: text.substr(0, 350), source: 'file-extension-tags' }); - const pager = await this.extensionsWorkbenchService.queryGallery(options); - return new PagedModel(pager); + return this.extensionsWorkbenchService.queryGallery(options).then(pager => new PagedModel(pager)); } } @@ -297,8 +330,7 @@ export class ExtensionsListView extends ViewletPanel { options.source = 'viewlet'; } - const pager = await this.extensionsWorkbenchService.queryGallery(options); - return new PagedModel(pager); + return this.extensionsWorkbenchService.queryGallery(options).then(pager => new PagedModel(pager)); } private sortExtensions(extensions: IExtension[], options: IQueryOptions): IExtension[] { @@ -326,14 +358,13 @@ export class ExtensionsListView extends ViewletPanel { return this.extensionsWorkbenchService.queryLocal() .then(result => result.filter(e => e.type === LocalExtensionType.User)) .then(local => { - const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); - let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); + const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); const othersPromise = this.tipsService.getOtherRecommendations(); const workspacePromise = this.tipsService.getWorkspaceRecommendations(); return TPromise.join([othersPromise, workspacePromise]) .then(([others, workspaceRecommendations]) => { - const names = this.getTrimmedRecommendations(installedExtensions, value, fileBasedRecommendations, others, workspaceRecommendations); + const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, others, workspaceRecommendations); const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); /* __GDPR__ "extensionAllRecommendations:open" : { @@ -363,24 +394,37 @@ export class ExtensionsListView extends ViewletPanel { }); } + private getCuratedModel(query: Query, options: IQueryOptions): TPromise> { + const value = query.value.replace(/curated:/g, '').trim(); + return this.experimentService.getCuratedExtensionsList(value).then(names => { + if (Array.isArray(names) && names.length) { + options.source = `curated:${value}`; + return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length })) + .then(pager => { + this.sortFirstPage(pager, names); + return new PagedModel(pager || []); + }); + } + return TPromise.as(new PagedModel([])); + }); + } + private getRecommendationsModel(query: Query, options: IQueryOptions): TPromise> { const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); return this.extensionsWorkbenchService.queryLocal() .then(result => result.filter(e => e.type === LocalExtensionType.User)) .then(local => { - const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); const othersPromise = this.tipsService.getOtherRecommendations(); const workspacePromise = this.tipsService.getWorkspaceRecommendations(); return TPromise.join([othersPromise, workspacePromise]) .then(([others, workspaceRecommendations]) => { - workspaceRecommendations = workspaceRecommendations.map(x => x.toLowerCase()); - fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.indexOf(x.toLowerCase()) === -1); - others = others.filter(x => workspaceRecommendations.indexOf(x.toLowerCase()) === -1); + fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); + others = others.filter(x => x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - const names = this.getTrimmedRecommendations(installedExtensions, value, fileBasedRecommendations, others, []); + const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, others, []); const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); /* __GDPR__ @@ -413,44 +457,48 @@ export class ExtensionsListView extends ViewletPanel { } // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions - private getTrimmedRecommendations(installedExtensions: string[], value: string, fileBasedRecommendations: string[], otherRecommendations: string[], workpsaceRecommendations: string[], ) { + private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workpsaceRecommendations: IExtensionRecommendation[]): string[] { const totalCount = 8; workpsaceRecommendations = workpsaceRecommendations - .filter(name => { - return installedExtensions.indexOf(name) === -1 - && name.toLowerCase().indexOf(value) > -1; + .filter(recommendation => { + return !this.isRecommendationInstalled(recommendation, installedExtensions) + && recommendation.extensionId.toLowerCase().indexOf(value) > -1; }); - fileBasedRecommendations = fileBasedRecommendations.filter(x => { - return installedExtensions.indexOf(x) === -1 - && workpsaceRecommendations.indexOf(x) === -1 - && x.toLowerCase().indexOf(value) > -1; + fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => { + return !this.isRecommendationInstalled(recommendation, installedExtensions) + && workpsaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) + && recommendation.extensionId.toLowerCase().indexOf(value) > -1; }); - otherRecommendations = otherRecommendations.filter(x => { - return installedExtensions.indexOf(x) === -1 - && fileBasedRecommendations.indexOf(x) === -1 - && workpsaceRecommendations.indexOf(x) === -1 - && x.toLowerCase().indexOf(value) > -1; + otherRecommendations = otherRecommendations.filter(recommendation => { + return !this.isRecommendationInstalled(recommendation, installedExtensions) + && fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId) + && workpsaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) + && recommendation.extensionId.toLowerCase().indexOf(value) > -1; }); - let otherCount = Math.min(2, otherRecommendations.length); - let fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workpsaceRecommendations.length - otherCount); - let names = workpsaceRecommendations; - names.push(...fileBasedRecommendations.splice(0, fileBasedCount)); - names.push(...otherRecommendations.splice(0, otherCount)); + const otherCount = Math.min(2, otherRecommendations.length); + const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workpsaceRecommendations.length - otherCount); + const recommendations = workpsaceRecommendations; + recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount)); + recommendations.push(...otherRecommendations.splice(0, otherCount)); - return names; + return distinct(recommendations.map(({ extensionId }) => extensionId)); + } + + private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean { + return installed.some(i => areSameExtensions({ id: i.id }, { id: recommendation.extensionId })); } private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions): TPromise> { const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); return this.tipsService.getWorkspaceRecommendations() .then(recommendations => { - const names = recommendations.filter(name => name.toLowerCase().indexOf(value) > -1); + const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1); /* __GDPR__ - "extensionWorkspaceRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ + "extensionWorkspaceRecommendations:open" : { + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length }); if (!names.length) { @@ -464,8 +512,8 @@ export class ExtensionsListView extends ViewletPanel { private getKeymapRecommendationsModel(query: Query, options: IQueryOptions): TPromise> { const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); - const names = this.tipsService.getKeymapRecommendations() - .filter(name => name.toLowerCase().indexOf(value) > -1); + const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId) + .filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1); if (!names.length) { return TPromise.as(new PagedModel([])); @@ -483,24 +531,26 @@ export class ExtensionsListView extends ViewletPanel { }); } - private setModel(model: IPagedModel) { - this.list.model = model; - this.list.scrollTop = 0; - const count = this.count(); + private setModel(model: IPagedModel, isGalleryError?: boolean) { + if (this.list) { + this.list.model = new DelayedPagedModel(model); + this.list.scrollTop = 0; + const count = this.count(); - toggleClass(this.extensionsList, 'hidden', count === 0); - toggleClass(this.messageBox, 'hidden', count > 0); - this.badge.setCount(count); + toggleClass(this.extensionsList, 'hidden', count === 0); + toggleClass(this.messageBox, 'hidden', count > 0); + this.badge.setCount(count); - if (count === 0 && this.isVisible()) { - this.messageBox.textContent = localize('no extensions found', "No extensions found."); - } else { - this.messageBox.textContent = ''; + if (count === 0 && this.isVisible()) { + this.messageBox.textContent = isGalleryError ? localize('galleryError', "We cannot connect to the Extensions Marketplace at this time, please try again later.") : localize('no extensions found', "No extensions found."); + } else { + this.messageBox.textContent = ''; + } } } private openExtension(extension: IExtension): void { - this.extensionsWorkbenchService.open(extension).done(null, err => this.onError(err)); + this.extensionsWorkbenchService.open(extension).then(null, err => this.onError(err)); } private pin(): void { @@ -533,8 +583,9 @@ export class ExtensionsListView extends ViewletPanel { } dispose(): void { - this.disposables = dispose(this.disposables); super.dispose(); + this.disposables = dispose(this.disposables); + this.list = null; } static isBuiltInExtensionsQuery(query: string): boolean { @@ -542,19 +593,11 @@ export class ExtensionsListView extends ViewletPanel { } static isInstalledExtensionsQuery(query: string): boolean { - return /@installed/i.test(query); + return /@installed|@outdated|@enabled|@disabled/i.test(query); } - static isOutdatedExtensionsQuery(query: string): boolean { - return /@outdated/i.test(query); - } - - static isDisabledExtensionsQuery(query: string): boolean { - return /@disabled/i.test(query); - } - - static isEnabledExtensionsQuery(query: string): boolean { - return /@enabled/i.test(query); + static isGroupByServersExtensionsQuery(query: string): boolean { + return !!Query.parse(query).groupBy; } static isRecommendedExtensionsQuery(query: string): boolean { @@ -572,57 +615,114 @@ export class ExtensionsListView extends ViewletPanel { static isKeymapsRecommendedExtensionsQuery(query: string): boolean { return /@recommended:keymaps/i.test(query); } + + focus(): void { + super.focus(); + if (!(this.list.getFocus().length || this.list.getSelection().length)) { + this.list.focusNext(); + } + this.list.domFocus(); + } } -export class InstalledExtensionsView extends ExtensionsListView { +export class GroupByServerExtensionsView extends ExtensionsListView { - public static isInstalledExtensionsQuery(query: string): boolean { - return ExtensionsListView.isInstalledExtensionsQuery(query) - || ExtensionsListView.isOutdatedExtensionsQuery(query) - || ExtensionsListView.isDisabledExtensionsQuery(query) - || ExtensionsListView.isEnabledExtensionsQuery(query); - } - - async show(query: string): TPromise> { - if (InstalledExtensionsView.isInstalledExtensionsQuery(query)) { - return super.show(query); + async show(query: string): Promise> { + query = query.replace(/@group:server/g, '').trim(); + query = query ? query : '@installed'; + if (!ExtensionsListView.isInstalledExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { + query = query += ' @installed'; } - let searchInstalledQuery = '@installed'; - searchInstalledQuery = query ? searchInstalledQuery + ' ' + query : searchInstalledQuery; - return super.show(searchInstalledQuery); + return super.show(query.trim()); + } +} + +export class EnabledExtensionsView extends ExtensionsListView { + private readonly enabledExtensionsQuery = '@enabled'; + + async show(query: string): Promise> { + return (query && query.trim() !== this.enabledExtensionsQuery) ? this.showEmptyModel() : super.show(this.enabledExtensionsQuery); + } +} + +export class DisabledExtensionsView extends ExtensionsListView { + private readonly disabledExtensionsQuery = '@disabled'; + + async show(query: string): Promise> { + return (query && query.trim() !== this.disabledExtensionsQuery) ? this.showEmptyModel() : super.show(this.disabledExtensionsQuery); } } export class BuiltInExtensionsView extends ExtensionsListView { - - async show(query: string): TPromise> { - return super.show(query.replace('@builtin', '@builtin:features')); + async show(query: string): Promise> { + return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:features'); } - } export class BuiltInThemesExtensionsView extends ExtensionsListView { - - async show(query: string): TPromise> { - return super.show(query.replace('@builtin', '@builtin:themes')); + async show(query: string): Promise> { + return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:themes'); } } export class BuiltInBasicsExtensionsView extends ExtensionsListView { - - async show(query: string): TPromise> { - return super.show(query.replace('@builtin', '@builtin:basics')); + async show(query: string): Promise> { + return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:basics'); } } -export class RecommendedExtensionsView extends ExtensionsListView { +export class DefaultRecommendedExtensionsView extends ExtensionsListView { + private readonly recommendedExtensionsQuery = '@recommended:all'; - async show(query: string): TPromise> { - return super.show(!query.trim() ? '@recommended:all' : '@recommended'); + renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.disposables.push(this.tipsService.onRecommendationChange(() => { + this.show(''); + })); + } + + async show(query: string): Promise> { + if (query && query.trim() !== this.recommendedExtensionsQuery) { + return this.showEmptyModel(); + } + const model = await super.show(this.recommendedExtensionsQuery); + if (!this.extensionsWorkbenchService.local.some(e => e.type === LocalExtensionType.User)) { + // This is part of popular extensions view. Collapse if no installed extensions. + this.setExpanded(model.length > 0); + } + return model; + } + +} + +export class RecommendedExtensionsView extends ExtensionsListView { + private readonly recommendedExtensionsQuery = '@recommended'; + + renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.disposables.push(this.tipsService.onRecommendationChange(() => { + this.show(''); + })); + } + + async show(query: string): Promise> { + return (query && query.trim() !== this.recommendedExtensionsQuery) ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery); } } export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { + private readonly recommendedExtensionsQuery = '@recommended:workspace'; + private installAllAction: InstallWorkspaceRecommendedExtensionsAction; + + renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.disposables.push(this.tipsService.onRecommendationChange(() => this.update())); + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall())); + this.disposables.push(this.contextService.onDidChangeWorkbenchState(() => this.update())); + } renderHeader(container: HTMLElement): void { super.renderHeader(container); @@ -634,22 +734,38 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { animated: false }); actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL); + + this.installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, []); const configureWorkspaceFolderAction = this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL); - installAllAction.class = 'octicon octicon-cloud-download'; + this.installAllAction.class = 'octicon octicon-cloud-download'; configureWorkspaceFolderAction.class = 'octicon octicon-pencil'; - actionbar.push([installAllAction], { icon: true, label: false }); + actionbar.push([this.installAllAction], { icon: true, label: false }); actionbar.push([configureWorkspaceFolderAction], { icon: true, label: false }); - this.disposables.push(actionbar); + this.disposables.push(...[this.installAllAction, configureWorkspaceFolderAction, actionbar]); } - async show(query: string): TPromise> { - let model = await super.show('@recommended:workspace'); + async show(query: string): Promise> { + let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; + let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); return model; } + private update(): void { + this.show(this.recommendedExtensionsQuery); + this.setRecommendationsToInstall(); + } + + private setRecommendationsToInstall(): TPromise { + return this.getRecommendationsToInstall() + .then(recommendations => { this.installAllAction.recommendations = recommendations; }); + } + + private getRecommendationsToInstall(): TPromise { + return this.tipsService.getWorkspaceRecommendations() + .then(recommendations => recommendations.filter(({ extensionId }) => !this.extensionsWorkbenchService.local.some(i => areSameExtensions({ id: extensionId }, { id: i.id })))); + } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/media/clear-inverse.svg b/src/vs/workbench/parts/extensions/electron-browser/media/clear-inverse.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/clear-inverse.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/clear-inverse.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/clear.svg b/src/vs/workbench/parts/extensions/electron-browser/media/clear.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/clear.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/clear.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/defaultIcon.png b/src/vs/workbench/parts/extensions/electron-browser/media/defaultIcon.png similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/defaultIcon.png rename to src/vs/workbench/parts/extensions/electron-browser/media/defaultIcon.png diff --git a/src/vs/workbench/parts/extensions/browser/media/extensionActions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css similarity index 90% rename from src/vs/workbench/parts/extensions/browser/media/extensionActions.css rename to src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css index fcfd494ab70..43bcde8a007 100644 --- a/src/vs/workbench/parts/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css @@ -19,6 +19,8 @@ background: url('clear-inverse.svg') center center no-repeat; } +.monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after, +.monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after, .monaco-action-bar .action-item .action-label.extension-action.enable:after, .monaco-action-bar .action-item .action-label.extension-action.disable:after { content: '▼'; @@ -69,4 +71,8 @@ .hc-black .extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage, .vs-dark .extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage { background: url('manage-inverse.svg') center center no-repeat; -} \ No newline at end of file +} + +.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container { + justify-content: flex-start; +} 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 42e53acdd09..7a8a61811e5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -14,16 +14,14 @@ .extension-editor > .header { display: flex; - height: 128px; - padding: 20px; + padding-top: 20px; + padding-bottom: 14px; + padding-left: 20px; + padding-right: 20px; overflow: hidden; font-size: 14px; } -.extension-editor > .header.recommended { - height: 140px; -} - .extension-editor > .header > .icon { height: 128px; width: 128px; @@ -31,7 +29,6 @@ } .extension-editor > .header > .details { - flex: 1; padding-left: 20px; overflow: hidden; user-select: text; @@ -128,14 +125,52 @@ padding: 1px 6px; } -.extension-editor > .header.recommended > .details > .recommendation { +.extension-editor > .header > .details > .recommendation { display: none; } -.extension-editor > .header.recommended > .details > .recommendation { +.extension-editor > .header.recommended > .details > .recommendation, +.extension-editor > .header.recommendation-ignored > .details > .recommendation { display: block; - margin-top: 2px; + float: left; + margin-top: 0; font-size: 13px; + font-style: italic; +} + +.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar, +.extension-editor > .header.recommendation-ignored > .details > .recommendation > .monaco-action-bar { + float: left; + margin-top: 2px; + font-style: normal; +} + +.extension-editor > .header > .details > .recommendation > .recommendation-text { + float:left; + margin-top: 5px; + margin-right: 4px; +} +.extension-editor > .header > .details > .recommendation > .monaco-action-bar .action-label { + margin-top: 4px; + margin-left: 4px; + padding-top: 0; + padding-bottom: 2px; +} + +.extension-editor > .header.recommendation-ignored > .details > .recommendation > .monaco-action-bar .ignore { + display: none; +} + +.extension-editor > .header.recommendation-ignored > .details > .recommendation > .monaco-action-bar .undo-ignore { + display: block; +} + +.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .ignore { + display: block; +} + +.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .undo-ignore { + display: none; } .extension-editor > .body { @@ -248,6 +283,7 @@ .extension-editor > .body > .content table th { text-align: left; + word-break: keep-all; } .extension-editor > .body > .content table code:not(:empty) { @@ -275,11 +311,11 @@ border-color: rgb(238, 238, 238); } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension { line-height: 62px; } -.extension-editor .subcontent .monaco-tree-row .content .unknown-dependency > .error-marker { +.extension-editor .subcontent .monaco-tree-row .content .unknown-extension > .error-marker { background-color: #BE1100; padding: 2px 4px; font-weight: bold; @@ -287,45 +323,46 @@ color: #CCC; } -.extension-editor .subcontent .monaco-tree-row .unknown-dependency > .message { +.extension-editor .subcontent .monaco-tree-row .unknown-extension > .message { padding-left: 10px; font-weight: bold; font-size: 14px; } -.extension-editor .subcontent .monaco-tree-row .dependency { +.extension-editor .subcontent .monaco-tree-row .extension { display: flex; align-items: center; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details { +.extension-editor .subcontent .monaco-tree-row .extension > .details { flex: 1; overflow: hidden; padding-left: 10px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header { display: flex; align-items: center; height: 19px; overflow: hidden; } -.extension-editor .subcontent .monaco-tree-row .dependency > .icon { +.extension-editor .subcontent .monaco-tree-row .extension > .icon { height: 40px; width: 40px; + object-fit: contain; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name { font-weight: bold; font-size: 16px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .name:hover { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name:hover { text-decoration: underline; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .header > .identifier { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .identifier { font-size: 90%; opacity: 0.6; margin-left: 10px; @@ -334,14 +371,14 @@ border-radius: 4px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer { display: flex; height: 19px; overflow: hidden; padding-top: 5px; } -.extension-editor .subcontent .monaco-tree-row .dependency > .details > .footer > .author { +.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer > .author { font-size: 90%; font-weight: 600; opacity: 0.6; 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 848c0382f9a..a879a47e71a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -6,4 +6,9 @@ .monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-dark.svg') no-repeat 50% 50%; -webkit-mask-size: 21px; +} + +.extensions .split-view-view .panel-header .count-badge-wrapper { + position: absolute; + right: 12px; } \ No newline at end of file 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 34324005e71..e8e11338689 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css @@ -44,7 +44,7 @@ } .extensions-viewlet > .extensions .panel-header { - padding-right: 12px; + padding-right: 28px; } .extensions-viewlet > .extensions .panel-header > .title { @@ -80,7 +80,7 @@ box-sizing: border-box; width: 100%; height: 100%; - padding: 0 11px 0 16px; + padding: 0 0 0 16px; overflow: hidden; display: flex; position: absolute; @@ -117,6 +117,7 @@ height: 19px; display: flex; overflow: hidden; + padding-right: 11px; } .extensions-viewlet > .extensions .extension > .details > .header-container > .header { @@ -162,9 +163,14 @@ display: none; } +.extensions-viewlet > .extensions .extension > .details > .description { + padding-right: 11px; +} + .extensions-viewlet > .extensions .extension > .details > .footer { display: flex; justify-content: flex-end; + padding-right: 7px; height: 24px; overflow: hidden; } @@ -173,16 +179,20 @@ flex: 1; font-size: 90%; padding-right: 6px; - opacity: 0.6; + opacity: 0.9; font-weight: 600; } +.extensions-viewlet > .extensions .selected .extension > .details > .footer > .author, +.extensions-viewlet > .extensions .selected.focused .extension > .details > .footer > .author { + opacity: 1; +} + .extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container { flex-wrap: wrap-reverse; } .extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar .action-label { - margin-right: 0; margin-top: 0.3em; margin-left: 0.3em; line-height: 14px; @@ -215,4 +225,4 @@ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); background-position: center center; background-repeat: no-repeat; -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg b/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg new file mode 100755 index 00000000000..de095424278 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/language-icon.svg @@ -0,0 +1 @@ +Market_LanguageGeneric \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/media/manage-inverse.svg b/src/vs/workbench/parts/extensions/electron-browser/media/manage-inverse.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/manage-inverse.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/manage-inverse.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/manage.svg b/src/vs/workbench/parts/extensions/electron-browser/media/manage.svg similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/manage.svg rename to src/vs/workbench/parts/extensions/electron-browser/media/manage.svg diff --git a/src/vs/workbench/parts/extensions/browser/media/theme-icon.png b/src/vs/workbench/parts/extensions/electron-browser/media/theme-icon.png similarity index 100% rename from src/vs/workbench/parts/extensions/browser/media/theme-icon.png rename to src/vs/workbench/parts/extensions/electron-browser/media/theme-icon.png diff --git a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts index 9ce9541ff34..470724893b1 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -9,7 +9,7 @@ import 'vs/css!./media/runtimeExtensionsEditor'; import * as nls from 'vs/nls'; import * as os from 'os'; import product from 'vs/platform/node/product'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { EditorInput } from 'vs/workbench/common/editor'; import pkg from 'vs/platform/node/package'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -21,7 +21,7 @@ import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/parts/exte import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService, IExtensionDescription, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -36,7 +36,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { memoize } from 'vs/base/common/decorators'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; -import { DisableForWorkspaceAction, DisableGloballyAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; +import { DisableForWorkspaceAction, DisableGloballyAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { INotificationService } from 'vs/platform/notification/common/notification'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); @@ -216,7 +216,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { const TEMPLATE_ID = 'runtimeExtensionElementTemplate'; - const delegate = new class implements IDelegate{ + const delegate = new class implements IVirtualDelegate{ getHeight(element: IRuntimeExtension): number { return 62; } @@ -318,6 +318,14 @@ export class RuntimeExtensionsEditor extends BaseEditor { ] }, "Activated because file {0} exists in your workspace", fileNameOrGlob); } + } else if (/^workspaceContainsTimeout:/.test(activationTimes.activationEvent)) { + const glob = activationTimes.activationEvent.substr('workspaceContainsTimeout:'.length); + title = nls.localize({ + key: 'workspaceContainsTimeout', + comment: [ + '{0} will be a glob pattern' + ] + }, "Activated because searching for {0} took too long", glob); } else if (/^onLanguage:/.test(activationTimes.activationEvent)) { let language = activationTimes.activationEvent.substr('onLanguage:'.length); title = nls.localize('languageActivation', "Activated because you opened a {0} file", language); @@ -369,6 +377,8 @@ export class RuntimeExtensionsEditor extends BaseEditor { } }, + disposeElement: () => null, + disposeTemplate: (data: IRuntimeExtensionTemplateData): void => { data.disposables = dispose(data.disposables); } @@ -443,7 +453,7 @@ export class RuntimeExtensionsInput extends EditorInput { return true; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } @@ -573,7 +583,11 @@ class SaveExtensionHostProfileAction extends Action { }); } - async run(): TPromise { + run(): TPromise { + return TPromise.wrap(this._asyncRun()); + } + + private async _asyncRun(): Promise { let picked = await this._windowService.showSaveDialog({ title: 'Save Extension Host Profile', buttonLabel: 'Save', diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index a33551eed19..28e53bddc82 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -8,10 +8,8 @@ import * as nls from 'vs/nls'; import { readFile } from 'vs/base/node/pfs'; import * as semver from 'semver'; -import * as path from 'path'; import { Event, Emitter } from 'vs/base/common/event'; import { index } from 'vs/base/common/arrays'; -import { assign } from 'vs/base/common/objects'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -20,23 +18,27 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, IExtensionManifest, - InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionTipsService, InstallOperation + InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionIdFromLocal, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowService } from 'vs/platform/windows/common/windows'; import Severity from 'vs/base/common/severity'; -import URI from 'vs/base/common/uri'; -import { IExtension, IExtensionDependencies, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { IExtension, IExtensionDependencies, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; 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 { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IProgressService2, ProgressLocation } from 'vs/workbench/services/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'; interface IExtensionStateProvider { (extension: Extension): T; @@ -44,14 +46,16 @@ 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 local: ILocalExtension, + public locals: ILocalExtension[], public gallery: IGalleryExtension, - private telemetryService: ITelemetryService + private telemetryService: ITelemetryService, + private logService: ILogService ) { } get type(): LocalExtensionType { @@ -117,14 +121,6 @@ class Extension implements IExtension { return `${product.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; } - get downloadUrl(): string { - if (!product.extensionsGallery) { - return null; - } - - return `${product.extensionsGallery.serviceUrl}/publishers/${this.publisher}/vsextensions/${this.name}/${this.latestVersion}/vspackage`; - } - get iconUrl(): string { return this.galleryIconUrl || this.localIconUrl || this.defaultIconUrl; } @@ -134,8 +130,10 @@ class Extension implements IExtension { } private get localIconUrl(): string { - return this.local && this.local.manifest.icon - && URI.file(path.join(this.local.path, this.local.manifest.icon)).toString(); + if (this.local && this.local.manifest.icon) { + return resources.joinPath(this.local.location, this.local.manifest.icon).toString(); + } + return null; } private get galleryIconUrl(): string { @@ -150,14 +148,14 @@ class Extension implements IExtension { if (this.type === LocalExtensionType.System) { 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('../browser/media/theme-icon.png'); + return require.toUrl('../electron-browser/media/theme-icon.png'); } - if (Array.isArray(this.local.manifest.contributes.languages) && this.local.manifest.contributes.languages.length) { - return require.toUrl('../browser/media/language-icon.png'); + if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) { + return require.toUrl('../electron-browser/media/language-icon.svg'); } } } - return require.toUrl('../browser/media/defaultIcon.png'); + return require.toUrl('../electron-browser/media/defaultIcon.png'); } get repository(): string { @@ -208,22 +206,34 @@ class Extension implements IExtension { return this.local && this.gallery && semver.gt(this.local.manifest.version, this.gallery.version); } - getManifest(): TPromise { + getManifest(token: CancellationToken): TPromise { if (this.gallery && !this.isGalleryOutdated()) { if (this.gallery.assets.manifest) { - return this.galleryService.getManifest(this.gallery); + return this.galleryService.getManifest(this.gallery, token); } - this.telemetryService.publicLog('extensions:NotFoundManifest', this.telemetryData); - return TPromise.wrapError(new Error('not available')); + this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.id); + return TPromise.as(undefined); } return TPromise.as(this.local.manifest); } - getReadme(): TPromise { + hasReadme(): boolean { + if (this.gallery && !this.isGalleryOutdated() && this.gallery.assets.readme) { + return true; + } + + if (this.local && this.local.readmeUrl) { + return true; + } + + return this.type === LocalExtensionType.System; + } + + getReadme(token: CancellationToken): TPromise { if (this.gallery && !this.isGalleryOutdated()) { if (this.gallery.assets.readme) { - return this.galleryService.getReadme(this.gallery); + return this.galleryService.getReadme(this.gallery, token); } this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData); } @@ -235,8 +245,8 @@ class Extension implements IExtension { if (this.type === LocalExtensionType.System) { return TPromise.as(`# ${this.displayName || this.name} -**Notice** This is a an extension that is bundled with Visual Studio Code. - +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. +## Features ${this.description} `); } @@ -244,14 +254,31 @@ ${this.description} return TPromise.wrapError(new Error('not available')); } - getChangelog(): TPromise { + hasChangelog(): boolean { if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { - return this.galleryService.getChangelog(this.gallery); + return true; + } + + if (this.local && this.local.changelogUrl) { + const uri = URI.parse(this.local.changelogUrl); + return uri.scheme === 'file'; + } + + return this.type === LocalExtensionType.System; + } + + getChangelog(token: CancellationToken): TPromise { + if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) { + return this.galleryService.getChangelog(this.gallery, token); } const changelogUrl = this.local && this.local.changelogUrl; if (!changelogUrl) { + if (this.type === LocalExtensionType.System) { + return TPromise.as('Please check the [VS Code Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.'); + } + return TPromise.wrapError(new Error('not available')); } @@ -267,13 +294,24 @@ ${this.description} get dependencies(): string[] { const { local, gallery } = this; if (gallery && !this.isGalleryOutdated()) { - return gallery.properties.dependencies; + return gallery.properties.dependencies || []; } if (local && local.manifest.extensionDependencies) { return local.manifest.extensionDependencies; } return []; } + + get extensionPack(): string[] { + const { local, gallery } = this; + if (gallery && !this.isGalleryOutdated()) { + return gallery.properties.extensionPack || []; + } + if (local && local.manifest.extensionPack) { + return local.manifest.extensionPack; + } + return []; + } } class ExtensionDependencies implements IExtensionDependencies { @@ -305,7 +343,7 @@ class ExtensionDependencies implements IExtensionDependencies { if (!this.hasDependencies) { return []; } - return this._extension.dependencies.map(d => new ExtensionDependencies(this._map.get(d), d, this._map, this)); + return this._extension.dependencies.map(id => new ExtensionDependencies(this._map.get(id), id, this._map, this)); } private computeHasDependencies(): boolean { @@ -326,7 +364,6 @@ class ExtensionDependencies implements IExtensionDependencies { export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, IURLHandler { private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours - _serviceBrand: any; private stateProvider: IExtensionStateProvider; private installing: Extension[] = []; @@ -336,8 +373,8 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, private autoUpdateDelayer: ThrottledDelayer; private disposables: IDisposable[] = []; - private readonly _onChange: Emitter = new Emitter(); - get onChange(): Event { return this._onChange.event; } + private readonly _onChange: Emitter = new Emitter(); + get onChange(): Event { return this._onChange.event; } private _extensionAllowedBadgeProviders: string[]; @@ -348,14 +385,14 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, @IConfigurationService private configurationService: IConfigurationService, @ITelemetryService private telemetryService: ITelemetryService, - @IDialogService private dialogService: IDialogService, @INotificationService private notificationService: INotificationService, @IURLService urlService: IURLService, @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, @IWindowService private windowService: IWindowService, @ILogService private logService: ILogService, @IProgressService2 private progressService: IProgressService2, - @IExtensionTipsService private extensionTipsService: IExtensionTipsService + @IExtensionService private runtimeExtensionService: IExtensionService, + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService ) { this.stateProvider = ext => this.getExtensionState(ext); @@ -376,9 +413,14 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, this.checkForUpdates(); } } + if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates(); + } + } }, this, this.disposables); - this.queryLocal().done(() => this.eventuallySyncWithGallery(true)); + this.queryLocal().then(() => this.eventuallySyncWithGallery(true)); } get local(): IExtension[] { @@ -390,61 +432,121 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } queryLocal(): TPromise { - return this.extensionService.getInstalled().then(result => { - const installedById = index(this.installed, e => e.local.identifier.id); - this.installed = result.map(local => { - const extension = installedById[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, local, null, this.telemetryService); - extension.local = local; - extension.enablementState = this.extensionEnablementService.getEnablementState(local); - return extension; - }); + return this.extensionService.getInstalled() + .then(installed => this.getDistinctInstalledExtensions(installed) + .then(distinctInstalled => { + const installedById = index(this.installed, e => e.local.identifier.id); + const groupById = groupBy(installed, i => getGalleryExtensionIdFromLocal(i)); + this.installed = distinctInstalled.map(local => { + const locals = groupById[getGalleryExtensionIdFromLocal(local)]; + locals.splice(locals.indexOf(local), 1); + locals.splice(0, 0, local); + const extension = installedById[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, locals, null, this.telemetryService, this.logService); + extension.locals = locals; + extension.enablementState = this.extensionEnablementService.getEnablementState(local); + return extension; + }); - this._onChange.fire(); - return this.local; - }); + this._onChange.fire(); + return this.local; + })); } queryGallery(options: IQueryOptions = {}): TPromise> { - return this.extensionService.getExtensionsReport().then(report => { - const maliciousSet = getMaliciousExtensionsSet(report); + return this.extensionService.getExtensionsReport() + .then(report => { + const maliciousSet = getMaliciousExtensionsSet(report); - return this.galleryService.query(options) - .then(result => mapPager(result, gallery => this.fromGallery(gallery, maliciousSet))) - .then(null, err => { - if (/No extension gallery service configured/.test(err.message)) { - return TPromise.as(singlePagePager([])); - } + return this.galleryService.query(options) + .then(result => mapPager(result, gallery => this.fromGallery(gallery, maliciousSet))) + .then(null, err => { + if (/No extension gallery service configured/.test(err.message)) { + return TPromise.as(singlePagePager([])); + } - return TPromise.wrapError>(err); - }); - }); + return TPromise.wrapError>(err); + }); + }); } - loadDependencies(extension: IExtension): TPromise { + loadDependencies(extension: IExtension, token: CancellationToken): TPromise { if (!extension.dependencies.length) { return TPromise.wrap(null); } - return this.extensionService.getExtensionsReport().then(report => { - const maliciousSet = getMaliciousExtensionsSet(report); + return this.extensionService.getExtensionsReport() + .then(report => { + const maliciousSet = getMaliciousExtensionsSet(report); - return this.galleryService.loadAllDependencies((extension).dependencies.map(id => { id })) - .then(galleryExtensions => galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet))) - .then(extensions => [...this.local, ...extensions]) - .then(extensions => { - const map = new Map(); - for (const extension of extensions) { - map.set(extension.id, extension); - } - return new ExtensionDependencies(extension, extension.id, map); - }); - }); + return this.galleryService.loadAllDependencies((extension).dependencies.map(id => ({ id })), token) + .then(galleryExtensions => galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet))) + .then(extensions => [...this.local, ...extensions]) + .then(extensions => { + const map = new Map(); + for (const extension of extensions) { + map.set(extension.id, extension); + } + return new ExtensionDependencies(extension, extension.id, map); + }); + }); } open(extension: IExtension, sideByside: boolean = false): TPromise { return this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), null, sideByside ? SIDE_GROUP : ACTIVE_GROUP); } + private getDistinctInstalledExtensions(allInstalled: ILocalExtension[]): TPromise { + if (!this.hasDuplicates(allInstalled)) { + return TPromise.as(allInstalled); + } + return TPromise.join([this.runtimeExtensionService.getExtensions(), this.extensionEnablementService.getDisabledExtensions()]) + .then(([runtimeExtensions, disabledExtensionIdentifiers]) => { + const groups = groupBy(allInstalled, (extension: ILocalExtension) => { + const isDisabled = disabledExtensionIdentifiers.some(identifier => areSameExtensions(identifier, { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid })); + 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[getGalleryExtensionIdFromLocal(extension)] = true; + } else { + notRunningExtensions.push(extension); + } + } + for (const extension of notRunningExtensions) { + if (!seenExtensions[getGalleryExtensionIdFromLocal(extension)]) { + enabled.push(extension); + seenExtensions[getGalleryExtensionIdFromLocal(extension)] = true; + } + } + const primaryDisabled = groups['disabled:primary'] || []; + const secondaryDisabled = (groups['disabled:secondary'] || []).filter(disabled => { + const identifier: IExtensionIdentifier = { id: getGalleryExtensionIdFromLocal(disabled), uuid: disabled.identifier.uuid }; + return primaryDisabled.every(p => !areSameExtensions({ id: getGalleryExtensionIdFromLocal(p), uuid: p.identifier.uuid }, identifier)); + }); + return [...enabled, ...primaryDisabled, ...secondaryDisabled]; + }); + } + + private hasDuplicates(extensions: ILocalExtension[]): boolean { + const seen: { [key: string]: boolean; } = Object.create(null); + for (const i of extensions) { + const key = getGalleryExtensionIdFromLocal(i); + if (seen[key]) { + return true; + } + seen[key] = true; + } + return false; + } + private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): Extension { let result = this.getInstalledExtensionMatchingGallery(gallery); @@ -458,7 +560,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, this.syncLocalWithGalleryExtension(result, gallery); } } else { - result = new Extension(this.galleryService, this.stateProvider, null, gallery, this.telemetryService); + result = new Extension(this.galleryService, this.stateProvider, [], gallery, this.telemetryService, this.logService); } if (maliciousExtensionSet.has(result.id)) { @@ -483,13 +585,13 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return null; } - private syncLocalWithGalleryExtension(local: Extension, gallery: IGalleryExtension) { + private syncLocalWithGalleryExtension(extension: Extension, gallery: IGalleryExtension) { // Sync the local extension with gallery extension if local extension doesnot has metadata - (local.local.metadata ? TPromise.as(local.local) : this.extensionService.updateMetadata(local.local, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId })) - .then(localExtension => { - local.local = localExtension; - local.gallery = gallery; - this._onChange.fire(); + TPromise.join(extension.locals.map(local => local.metadata ? TPromise.as(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(); }); } @@ -502,12 +604,17 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return this.configurationService.getValue(AutoUpdateConfigurationKey); } + private isAutoCheckUpdatesEnabled(): boolean { + return this.configurationService.getValue(AutoCheckUpdatesConfigurationKey); + } + private eventuallySyncWithGallery(immediate = false): void { - const loop = () => this.syncWithGallery().then(() => this.eventuallySyncWithGallery()); + const shouldSync = this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled(); + const loop = () => (shouldSync ? this.syncWithGallery() : TPromise.as(null)).then(() => this.eventuallySyncWithGallery()); const delay = immediate ? 0 : ExtensionsWorkbenchService.SyncPeriod; this.syncDelayer.trigger(loop, delay) - .done(null, err => null); + .then(null, err => null); } private syncWithGallery(): TPromise { @@ -535,7 +642,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, private eventuallyAutoUpdateExtensions(): void { this.autoUpdateDelayer.trigger(() => this.autoUpdateExtensions()) - .done(null, err => null); + .then(null, err => null); } private autoUpdateExtensions(): TPromise { @@ -565,11 +672,11 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, location: ProgressLocation.Extensions, title: nls.localize('installingVSIXExtension', 'Installing extension from VSIX...'), source: `${extension}` - }, () => this.extensionService.install(extension).then(() => null)); + }, () => this.extensionService.install(URI.file(extension)).then(() => null)); } if (!(extension instanceof Extension)) { - return undefined; + return TPromise.as(undefined); } if (extension.isMalicious) { @@ -587,7 +694,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, location: ProgressLocation.Extensions, title: nls.localize('installingMarketPlaceExtension', 'Installing extension from Marketplace....'), source: `${extension.id}` - }, () => this.extensionService.installFromGallery(gallery).then(() => null)); + }, () => this.extensionService.installFromGallery(gallery)); } setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): TPromise { @@ -601,9 +708,9 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } const ext = extension as Extension; - const local = ext.local || this.installed.filter(e => e.id === extension.id)[0].local; + const toUninstall: ILocalExtension[] = ext.locals.length ? ext.locals : this.installed.filter(e => e.id === extension.id)[0].locals; - if (!local) { + if (!toUninstall.length) { return TPromise.wrapError(new Error('Missing local')); } @@ -611,8 +718,8 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return this.progressService.withProgress({ location: ProgressLocation.Extensions, title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), - source: `${local.identifier.id}` - }, () => this.extensionService.uninstall(local)); + source: `${toUninstall[0].identifier.id}` + }, () => TPromise.join(toUninstall.map(local => this.extensionService.uninstall(local))).then(() => null)); } reinstall(extension: IExtension): TPromise { @@ -621,97 +728,71 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } const ext = extension as Extension; - const local = ext.local || this.installed.filter(e => e.id === extension.id)[0].local; + const toReinstall: ILocalExtension[] = ext.locals.length ? ext.locals : this.installed.filter(e => e.id === extension.id)[0].locals; - if (!local) { + if (!toReinstall.length) { return TPromise.wrapError(new Error('Missing local')); } return this.progressService.withProgress({ location: ProgressLocation.Extensions, - source: `${local.identifier.id}` - }, () => this.extensionService.reinstallFromGallery(local).then(() => null)); + source: `${toReinstall[0].identifier.id}` + }, () => TPromise.join(toReinstall.map(local => this.extensionService.reinstallFromGallery(local))).then(() => null)); } private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): TPromise { - const allDependencies = this.getDependenciesRecursively(extensions, this.local, enablementState, []); - if (allDependencies.length > 0) { - if (enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled) { - return this.promptForDependenciesAndEnable(extensions, allDependencies, enablementState); - } else { - return this.promptForDependenciesAndDisable(extensions, allDependencies, enablementState); + const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled; + if (enable) { + const allDependenciesAndPackedExtensions = this.getExtensionsRecursively(extensions, this.local, enablementState, { dependencies: true, pack: true }); + return this.checkAndSetEnablement(extensions, allDependenciesAndPackedExtensions, enablementState); + } else { + const packedExtensions = this.getExtensionsRecursively(extensions, this.local, enablementState, { dependencies: false, pack: true }); + if (packedExtensions.length) { + return this.checkAndSetEnablement(extensions, packedExtensions, enablementState); } + return this.checkAndSetEnablement(extensions, [], enablementState); } - return this.checkAndSetEnablement(extensions, [], enablementState); } - private promptForDependenciesAndEnable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('enableDependeciesConfirmation', "Enabling an extension also enables its dependencies. Would you like to continue?"); - const buttons = [ - nls.localize('enable', "Yes"), - nls.localize('doNotEnable', "No") - ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 1 }) - .then(value => { - if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); - } - return TPromise.as(null); - }); - } - - private promptForDependenciesAndDisable(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const message = nls.localize('disableDependeciesConfirmation', "Would you like to disable the dependencies of the extensions also?"); - const buttons = [ - nls.localize('yes', "Yes"), - nls.localize('no', "No"), - nls.localize('cancel', "Cancel") - ]; - return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 }) - .then(value => { - if (value === 0) { - return this.checkAndSetEnablement(extensions, dependencies, enablementState); - } - if (value === 1) { - return this.checkAndSetEnablement(extensions, [], enablementState); - } - return TPromise.as(null); - }); - } - - private checkAndSetEnablement(extensions: IExtension[], dependencies: IExtension[], enablementState: EnablementState): TPromise { - const allExtensions = [...extensions, ...dependencies]; + private checkAndSetEnablement(extensions: IExtension[], otherExtensions: IExtension[], enablementState: EnablementState): TPromise { + const allExtensions = [...extensions, ...otherExtensions]; const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled; if (!enable) { for (const extension of extensions) { let dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local, enablementState); if (dependents.length) { - return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, dependents))); + return TPromise.wrapError(new Error(this.getDependentsErrorMessage(extension, allExtensions, dependents))); } } } return TPromise.join(allExtensions.map(e => this.doSetEnablement(e, enablementState))); } - private getDependenciesRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, checked: IExtension[]): IExtension[] { + private getExtensionsRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, options: { dependencies: boolean, pack: boolean }, checked: IExtension[] = []): IExtension[] { const toCheck = extensions.filter(e => checked.indexOf(e) === -1); if (toCheck.length) { for (const extension of toCheck) { checked.push(extension); } - const dependenciesToDisable = installed.filter(i => { + const extensionsToDisable = installed.filter(i => { if (checked.indexOf(i) !== -1) { return false; } if (i.enablementState === enablementState) { return false; } - return i.type === LocalExtensionType.User && extensions.some(extension => extension.dependencies.indexOf(i.id) !== -1); + const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled; + return (enable || i.type === LocalExtensionType.User) // Include all Extensions for enablement and only user extensions for disablement + && (options.dependencies || options.pack) + && extensions.some(extension => + (options.dependencies && extension.dependencies.some(id => areSameExtensions({ id }, i))) + || (options.pack && extension.extensionPack.some(id => areSameExtensions({ id }, i))) + ); }); - if (dependenciesToDisable.length) { - const depsOfDeps = this.getDependenciesRecursively(dependenciesToDisable, installed, enablementState, checked); - return [...dependenciesToDisable, ...depsOfDeps]; + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getExtensionsRecursively(extensionsToDisable, installed, enablementState, options, checked)); } + return extensionsToDisable; } return []; } @@ -730,16 +811,21 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, if (extensionsToDisable.indexOf(i) !== -1) { return false; } - return i.dependencies.some(dep => { - if (extension.id === dep) { - return true; - } - return extensionsToDisable.some(d => d.id === dep); - }); + return i.dependencies.some(dep => [extension, ...extensionsToDisable].some(d => d.id === dep)); }); } - private getDependentsErrorMessage(extension: IExtension, dependents: IExtension[]): string { + private getDependentsErrorMessage(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string { + for (const e of [extension, ...allDisabledExtensions]) { + let dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e))); + if (dependentsOfTheExtension.length) { + return this.getErrorMessageForDisablingAnExtensionWithDependents(e, dependentsOfTheExtension); + } + } + return ''; + } + + private getErrorMessageForDisablingAnExtensionWithDependents(extension: IExtension, dependents: IExtension[]): string { if (dependents.length === 1) { return nls.localize('singleDependentError', "Cannot disable extension '{0}'. Extension '{1}' depends on this.", extension.displayName, dependents[0].displayName); } @@ -792,37 +878,42 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, let extension = this.installed.filter(e => areSameExtensions(e, gallery.identifier))[0]; if (!extension) { - extension = new Extension(this.galleryService, this.stateProvider, null, gallery, this.telemetryService); + extension = new Extension(this.galleryService, this.stateProvider, [], gallery, this.telemetryService, this.logService); } extension.gallery = gallery; this.installing.push(extension); - this._onChange.fire(); + this._onChange.fire(extension); } private onDidInstallExtension(event: DidInstallExtensionEvent): void { const { local, zipPath, error, gallery } = event; const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e, gallery.identifier))[0] : null; - const extension: Extension = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, local, null, this.telemetryService) : null; + let extension: Extension = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, [local], null, this.telemetryService, this.logService) : null; if (extension) { this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; if (!error) { const installed = this.installed.filter(e => e.id === extension.id)[0]; - extension.local = local; if (installed) { - installed.local = local; + extension = installed; + const server = this.extensionManagementServerService.getExtensionManagementServer(local.location); + const existingLocal = installed.locals.filter(l => this.extensionManagementServerService.getExtensionManagementServer(l.location).authority === server.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); } } - if (extension.gallery && event.operation === InstallOperation.Install) { - // Report recommendation telemetry only for gallery extensions that are first time installs - this.reportExtensionRecommendationsTelemetry(installingExtension); - } } - this._onChange.fire(); + this._onChange.fire(extension); } private onUninstallExtension({ id }: IExtensionIdentifier): void { @@ -861,7 +952,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, const enablementState = this.extensionEnablementService.getEnablementState(extension.local); if (enablementState !== extension.enablementState) { extension.enablementState = enablementState; - this._onChange.fire(); + this._onChange.fire(extension); } } } @@ -879,24 +970,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return local ? ExtensionState.Installed : ExtensionState.Uninstalled; } - private reportExtensionRecommendationsTelemetry(extension: Extension): void { - const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason() || {}; - const recommendationReason = extRecommendations[extension.id.toLowerCase()]; - if (recommendationReason) { - const recommendationsData = { recommendationReason: recommendationReason.reasonId }; - const data = extension.telemetryData; - /* __GDPR__ - "extensionGallery:install:recommendations" : { - "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - this.telemetryService.publicLog('extensionGallery:install:recommendations', assign(data, recommendationsData)); - } - } - private onError(err: any): void { if (isPromiseCanceledError(err)) { return; @@ -911,13 +984,13 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, this.notificationService.error(err); } - async handleURL(uri: URI): TPromise { + handleURL(uri: URI): TPromise { if (!/^extension/.test(uri.path)) { - return false; + return TPromise.as(false); } this.onOpenExtensionUrl(uri); - return true; + return TPromise.as(true); } private onOpenExtensionUrl(uri: URI): void { @@ -951,17 +1024,17 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, nls.localize('installConfirmation', "Would you like to install the '{0}' extension?", extension.displayName, extension.publisher), [{ label: nls.localize('install', "Install"), - run: () => this.install(extension).done(undefined, error => this.onError(error)) + run: () => this.install(extension).then(undefined, error => this.onError(error)) }] ); }); }); }); - }).done(undefined, error => this.onError(error)); + }).then(undefined, error => this.onError(error)); } dispose(): void { this.syncDelayer.cancel(); this.disposables = dispose(this.disposables); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts b/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts index 12d20be6399..f107018c53d 100644 --- a/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts +++ b/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts @@ -72,72 +72,80 @@ suite('Extension query', () => { }); test('toString', () => { - let query = new Query('hello', ''); + let query = new Query('hello', '', ''); assert.equal(query.toString(), 'hello'); - query = new Query('hello world', ''); + query = new Query('hello world', '', ''); assert.equal(query.toString(), 'hello world'); - query = new Query(' hello ', ''); + query = new Query(' hello ', '', ''); assert.equal(query.toString(), 'hello'); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert.equal(query.toString(), '@sort:installs'); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert.equal(query.toString(), '@sort:installs'); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert.equal(query.toString(), '@sort:installs'); - query = new Query('hello', 'installs'); + query = new Query('hello', 'installs', ''); assert.equal(query.toString(), 'hello @sort:installs'); - query = new Query(' hello ', 'installs'); + query = new Query(' hello ', 'installs', ''); assert.equal(query.toString(), 'hello @sort:installs'); }); test('isValid', () => { - let query = new Query('hello', ''); + let query = new Query('hello', '', ''); assert(query.isValid()); - query = new Query('hello world', ''); + query = new Query('hello world', '', ''); assert(query.isValid()); - query = new Query(' hello ', ''); + query = new Query(' hello ', '', ''); assert(query.isValid()); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert(query.isValid()); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert(query.isValid()); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert(query.isValid()); - query = new Query('', 'installs'); + query = new Query('', 'installs', ''); assert(query.isValid()); - query = new Query('hello', 'installs'); + query = new Query('hello', 'installs', ''); assert(query.isValid()); - query = new Query(' hello ', 'installs'); + query = new Query(' hello ', 'installs', ''); assert(query.isValid()); }); test('equals', () => { - let query1 = new Query('hello', ''); - let query2 = new Query('hello', ''); + let query1 = new Query('hello', '', ''); + let query2 = new Query('hello', '', ''); assert(query1.equals(query2)); - query2 = new Query('hello world', ''); + query2 = new Query('hello world', '', ''); assert(!query1.equals(query2)); - query2 = new Query('hello', 'installs'); + query2 = new Query('hello', 'installs', ''); assert(!query1.equals(query2)); - query2 = new Query('hello', 'installs'); + query2 = new Query('hello', 'installs', ''); assert(!query1.equals(query2)); }); + + test('autocomplete', () => { + Query.suggestions('@sort:in').some(x => x === '@sort:installs '); + Query.suggestions('@sort:installs').every(x => x !== '@sort:rating '); + + Query.suggestions('@category:blah').some(x => x === '@category:"extension packs" '); + Query.suggestions('@category:"extension packs"').every(x => x !== '@category:formatters '); + }); }); \ No newline at end of file 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 dbb1457fbdb..3bb3461aec4 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 @@ -10,13 +10,13 @@ import { assign } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; import { TPromise } from 'vs/base/common/winjs.base'; import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; -import * as ExtensionsActions from 'vs/workbench/parts/extensions/browser/extensionsActions'; +import * as ExtensionsActions from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, LocalExtensionType, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService, getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; @@ -34,6 +34,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { URLService } from 'vs/platform/url/common/urlService'; +import { URI } from 'vs/base/common/uri'; +import { SingleServerExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; suite('ExtensionsActions Test', () => { @@ -57,29 +60,31 @@ suite('ExtensionsActions Test', () => { instantiationService.stub(IWindowService, TestWindowService); instantiationService.stub(IWorkspaceContextService, new TestContextService()); - instantiationService.stub(IConfigurationService, { onDidUpdateConfiguration: () => { }, onDidChangeConfiguration: () => { }, getConfiguration: () => ({}) }); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); + instantiationService.stub(IExtensionManagementService, ExtensionManagementService); instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(SingleServerExtensionManagementServerService, { authority: 'vscode-local', extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local' })); + instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - instantiationService.stub(IExtensionTipsService, ExtensionTipsService); - instantiationService.stub(IExtensionTipsService, 'getKeymapRecommendations', () => []); + instantiationService.set(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); instantiationService.stub(IURLService, URLService); }); - setup(() => { + setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stub(IExtensionService, { getExtensions: () => TPromise.wrap([]) }); - (instantiationService.get(IExtensionEnablementService)).reset(); + await (instantiationService.get(IExtensionEnablementService)).reset(); instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); }); @@ -983,7 +988,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is newly installed', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1000,7 +1005,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is installed and uninstalled', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1018,7 +1023,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is uninstalled', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1035,7 +1040,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is uninstalled and installed', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.0' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.0', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1055,7 +1060,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is updated while running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.1' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.1', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const local = aLocalExtension('a', { version: '1.0.1' }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1075,7 +1080,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is updated when not running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const local = aLocalExtension('a', { version: '1.0.1' }); return instantiationService.get(IExtensionEnablementService).setEnablement(local, EnablementState.Disabled) .then(() => { @@ -1096,7 +1101,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is disabled when running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1113,7 +1118,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension enablement is toggled when running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.0' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.a', version: '1.0.0', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); @@ -1128,7 +1133,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is enabled when not running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const local = aLocalExtension('a'); return instantiationService.get(IExtensionEnablementService).setEnablement(local, EnablementState.Disabled) .then(() => { @@ -1149,7 +1154,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension enablement is toggled when not running', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const local = aLocalExtension('a'); return instantiationService.get(IExtensionEnablementService).setEnablement(local, EnablementState.Disabled) .then(() => { @@ -1167,7 +1172,7 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is updated when not running and enabled', () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b' }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ id: 'pub.b', extensionLocation: URI.file('pub.b') }]); const local = aLocalExtension('a', { version: '1.0.1' }); return instantiationService.get(IExtensionEnablementService).setEnablement(local, EnablementState.Disabled) .then(() => { @@ -1192,12 +1197,17 @@ suite('ExtensionsActions Test', () => { }); }); + test(`RecommendToFolderAction`, () => { + // TODO: Implement test + }); + function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension { const localExtension = Object.create({ manifest: {} }); - assign(localExtension, { type: LocalExtensionType.User, manifest: {} }, properties); + assign(localExtension, { type: LocalExtensionType.User, manifest: {}, location: URI.file(`pub.${name}`) }, properties); assign(localExtension.manifest, { name, publisher: 'pub', version: '1.0.0' }, manifest); localExtension.identifier = { id: getLocalExtensionIdFromManifest(localExtension.manifest) }; localExtension.metadata = { id: localExtension.identifier.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; + localExtension.galleryIdentifier = { id: getGalleryExtensionIdFromLocal(localExtension), uuid: void 0 }; return localExtension; } @@ -1214,4 +1224,4 @@ suite('ExtensionsActions Test', () => { return { firstPage: objects, total: objects.length, pageSize: objects.length, getPage: () => null }; } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index 3f7af9a154c..7bba8e405d0 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -5,6 +5,7 @@ 'use strict'; +import * as sinon from 'sinon'; import * as assert from 'assert'; import * as path from 'path'; import * as fs from 'fs'; @@ -26,7 +27,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { TestTextResourceConfigurationService, TestContextService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; @@ -36,10 +37,9 @@ import { IPager } from 'vs/base/common/paging'; import { assign } from 'vs/base/common/objects'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionsWorkbenchService, ConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; import { IURLService } from 'vs/platform/url/common/url'; import product from 'vs/platform/node/product'; @@ -48,6 +48,8 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; import { URLService } from 'vs/platform/url/common/urlService'; +import { IExperimentService } from 'vs/workbench/parts/experiments/node/experimentService'; +import { TestExperimentService } from 'vs/workbench/parts/experiments/test/node/experimentService.test'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -166,7 +168,6 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP suite('ExtensionsTipsService Test', () => { let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; - let extensionsWorkbenchService: IExtensionsWorkbenchService; let testConfigurationService: TestConfigurationService; let testObject: ExtensionTipsService; let parentResource: string; @@ -176,6 +177,7 @@ suite('ExtensionsTipsService Test', () => { didUninstallEvent: Emitter; let prompted: boolean; let onModelAddedEvent: Emitter; + let experimentService: TestExperimentService; suiteSetup(() => { instantiationService = new TestInstantiationService(); @@ -197,6 +199,9 @@ suite('ExtensionsTipsService Test', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, URLService); + experimentService = instantiationService.createInstance(TestExperimentService); + instantiationService.stub(IExperimentService, experimentService); + onModelAddedEvent = new Emitter(); product.extensionTips = { @@ -216,13 +221,17 @@ suite('ExtensionsTipsService Test', () => { }; }); + suiteTeardown(() => { + if (experimentService) { + experimentService.dispose(); + } + }); + setup(() => { instantiationService.stub(IEnvironmentService, { extensionDevelopmentPath: false }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stub(IExtensionGalleryService, 'isEnabled', true); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...mockExtensionGallery)); - extensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); - instantiationService.stub(IExtensionsWorkbenchService, extensionsWorkbenchService); prompted = false; @@ -243,27 +252,29 @@ suite('ExtensionsTipsService Test', () => { }); }); - teardown((done) => { + teardown(done => { (testObject).dispose(); - (extensionsWorkbenchService).dispose(); if (parentResource) { extfs.del(parentResource, os.tmpdir(), () => { }, done); + } else { + done(); } }); - function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[]): TPromise { + function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): TPromise { const id = uuid.generateUuid(); parentResource = path.join(os.tmpdir(), 'vsctests', id); - return setUpFolder(folderName, parentResource, recommendedExtensions); + return setUpFolder(folderName, parentResource, recommendedExtensions, ignoredRecommendations); } - function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[]): TPromise { + function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): TPromise { const folderDir = path.join(parentDir, folderName); const workspaceSettingsDir = path.join(folderDir, '.vscode'); return mkdirp(workspaceSettingsDir, 493).then(() => { const configPath = path.join(workspaceSettingsDir, 'extensions.json'); fs.writeFileSync(configPath, JSON.stringify({ - 'recommendations': recommendedExtensions + 'recommendations': recommendedExtensions, + 'unwantedRecommendations': ignoredRecommendations, }, null, '\t')); const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); @@ -276,7 +287,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.promptWorkspaceRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -286,7 +297,7 @@ suite('ExtensionsTipsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - assert.equal(!testObject.promptWorkspaceRecommendationsPromise, true); + assert.equal(!testObject.loadWorkspaceConfigPromise, true); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -297,12 +308,15 @@ suite('ExtensionsTipsService Test', () => { } test('ExtensionTipsService: No Prompt for valid workspace recommendations when galleryService is absent', () => { - instantiationService.stub(IExtensionGalleryService, 'isEnabled', false); - return testNoPromptOrRecommendationsForValidRecommendations(mockTestData.validRecommendedExtensions); + const galleryQuerySpy = sinon.spy(); + instantiationService.stub(IExtensionGalleryService, { query: galleryQuerySpy, isEnabled: () => false }); + + return testNoPromptOrRecommendationsForValidRecommendations(mockTestData.validRecommendedExtensions) + .then(() => assert.ok(galleryQuerySpy.notCalled)); }); test('ExtensionTipsService: No Prompt for valid workspace recommendations during extension development', () => { - instantiationService.stub(IEnvironmentService, { extensionDevelopmentPath: true }); + instantiationService.stub(IEnvironmentService, { extensionDevelopmentLocationURI: true }); return testNoPromptOrRecommendationsForValidRecommendations(mockTestData.validRecommendedExtensions); }); @@ -313,7 +327,7 @@ suite('ExtensionsTipsService Test', () => { test('ExtensionTipsService: Prompt for valid workspace recommendations', () => { return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.promptWorkspaceRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); @@ -345,7 +359,7 @@ suite('ExtensionsTipsService Test', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - return testObject.promptWorkspaceRecommendationsPromise.then(() => { + return testObject.loadWorkspaceConfigPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, 0); assert.ok(!prompted); }); @@ -357,17 +371,158 @@ suite('ExtensionsTipsService Test', () => { return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions); }); + test('ExtensionTipsService: No Recommendations of globally ignored recommendations', () => { + const storageGetterStub = (a, _, c) => { + const storedRecommendations = '["ms-vscode.csharp", "ms-python.python", "eg2.tslint"]'; + const ignoredRecommendations = '["ms-vscode.csharp", "mockpublisher2.mockextension2"]'; // ignore a stored recommendation and a workspace recommendation. + if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; } + if (a === 'extensionsAssistant/ignored_recommendations') { return ignoredRecommendations; } + return c; + }; + + instantiationService.stub(IStorageService, { + get: storageGetterStub, + getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c + }); + + return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been globally ignored + assert.ok(recommendations['ms-python.python']); // stored recommendation + assert.ok(recommendations['mockpublisher1.mockextension1']); // workspace recommendation + assert.ok(!recommendations['mockpublisher2.mockextension2']); // workspace recommendation that has been globally ignored + }); + }); + }); + + test('ExtensionTipsService: No Recommendations of workspace ignored recommendations', () => { + const ignoredRecommendations = ['ms-vscode.csharp', 'mockpublisher2.mockextension2']; // ignore a stored recommendation and a workspace recommendation. + const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]'; + instantiationService.stub(IStorageService, { + get: (a, b, c) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c, + getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c + }); + + return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(!recommendations['ms-vscode.csharp']); // stored recommendation that has been workspace ignored + assert.ok(recommendations['ms-python.python']); // stored recommendation + assert.ok(recommendations['mockpublisher1.mockextension1']); // workspace recommendation + assert.ok(!recommendations['mockpublisher2.mockextension2']); // workspace recommendation that has been workspace ignored + }); + }); + }); + + test('ExtensionTipsService: Able to retrieve collection of all ignored recommendations', () => { + + const storageGetterStub = (a, _, c) => { + const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]'; + const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation. + if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; } + if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; } + return c; + }; + + const workspaceIgnoredRecommendations = ['ms-vscode.csharp']; // ignore a stored recommendation and a workspace recommendation. + instantiationService.stub(IStorageService, { + get: storageGetterStub, + getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c + }); + + return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(recommendations['ms-python.python']); + + assert.ok(!recommendations['mockpublisher2.mockextension2']); + assert.ok(!recommendations['ms-vscode.csharp']); + }); + }); + }); + + test('ExtensionTipsService: Able to dynamically ignore/unignore global recommendations', () => { + const storageGetterStub = (a, _, c) => { + const storedRecommendations = '["ms-vscode.csharp", "ms-python.python"]'; + const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation. + if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; } + if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; } + return c; + }; + + instantiationService.stub(IStorageService, { + get: storageGetterStub, + store: () => { }, + getBoolean: (a, _, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c + }); + + return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(recommendations['ms-python.python']); + assert.ok(recommendations['mockpublisher1.mockextension1']); + + assert.ok(!recommendations['mockpublisher2.mockextension2']); + + return testObject.toggleIgnoredRecommendation('mockpublisher1.mockextension1', true); + }).then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(recommendations['ms-python.python']); + + assert.ok(!recommendations['mockpublisher1.mockextension1']); + assert.ok(!recommendations['mockpublisher2.mockextension2']); + + return testObject.toggleIgnoredRecommendation('mockpublisher1.mockextension1', false); + }).then(() => { + const recommendations = testObject.getAllRecommendationsWithReason(); + assert.ok(recommendations['ms-python.python']); + + assert.ok(recommendations['mockpublisher1.mockextension1']); + assert.ok(!recommendations['mockpublisher2.mockextension2']); + }); + }); + }); + + test('test global extensions are modified and recommendation change event is fired when an extension is ignored', () => { + const storageSetterTarget = sinon.spy(); + const changeHandlerTarget = sinon.spy(); + const ignoredExtensionId = 'Some.Extension'; + instantiationService.stub(IStorageService, { + get: (a, b, c) => a === 'extensionsAssistant/ignored_recommendations' ? '["ms-vscode.vscode"]' : c, + store: (...args) => { + storageSetterTarget(...args); + } + }); + + return setUpFolderWorkspace('myFolder', []).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + testObject.onRecommendationChange(changeHandlerTarget); + testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); + + assert.ok(changeHandlerTarget.calledOnce); + assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: 'Some.Extension', isRecommended: false })); + assert.ok(storageSetterTarget.calledWithExactly('extensionsAssistant/ignored_recommendations', `["ms-vscode.vscode","${ignoredExtensionId.toLowerCase()}"]`, StorageScope.GLOBAL)); + }); + }); + test('ExtensionTipsService: Get file based recommendations from storage (old format)', () => { const storedRecommendations = '["ms-vscode.csharp", "ms-python.python", "eg2.tslint"]'; instantiationService.stub(IStorageService, { get: (a, b, c) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c }); return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - const recommendations = testObject.getFileBasedRecommendations(); - assert.equal(recommendations.length, 2); - assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips - assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips - assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getFileBasedRecommendations(); + assert.equal(recommendations.length, 2); + assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips + assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips + assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'eg2.tslint')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips + }); }); }); @@ -380,12 +535,14 @@ suite('ExtensionsTipsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionTipsService); - const recommendations = testObject.getFileBasedRecommendations(); - assert.equal(recommendations.length, 2); - assert.ok(recommendations.indexOf('ms-vscode.csharp') > -1); // stored recommendation that exists in product.extensionTips - assert.ok(recommendations.indexOf('ms-python.python') > -1); // stored recommendation that exists in product.extensionImportantTips - assert.ok(recommendations.indexOf('eg2.tslint') === -1); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips - assert.ok(recommendations.indexOf('lukehoban.Go') === -1); //stored recommendation that is older than a week + return testObject.loadWorkspaceConfigPromise.then(() => { + const recommendations = testObject.getFileBasedRecommendations(); + assert.equal(recommendations.length, 2); + assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-vscode.csharp')); // stored recommendation that exists in product.extensionTips + assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips + assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'eg2.tslint')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips + assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'lukehoban.Go')); //stored recommendation that is older than a week + }); }); }); }); 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 40c885f86e0..3c2dbfeb066 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 @@ -11,13 +11,13 @@ import * as fs from 'fs'; import { assign } from 'vs/base/common/objects'; import { TPromise } from 'vs/base/common/winjs.base'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, LocalExtensionType, IGalleryExtension, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, EnablementState, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService, getLocalExtensionIdFromGallery, getLocalExtensionIdFromManifest } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/common/extensionEnablementService.test'; @@ -33,13 +33,14 @@ import { TestContextService, TestWindowService } from 'vs/workbench/test/workben import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWindowService } from 'vs/platform/windows/common/windows'; -import { IProgressService2 } from 'vs/platform/progress/common/progress'; +import { IProgressService2 } from 'vs/workbench/services/progress/common/progress'; import { ProgressService2 } from 'vs/workbench/services/progress/browser/progressService2'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { URLService } from 'vs/platform/url/common/urlService'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; -suite('ExtensionsWorkbenchService Test', () => { +suite('ExtensionsWorkbenchServiceTest', () => { let instantiationService: TestInstantiationService; let testObject: IExtensionsWorkbenchService; @@ -65,7 +66,14 @@ suite('ExtensionsWorkbenchService Test', () => { instantiationService.stub(IURLService, URLService); instantiationService.stub(IWorkspaceContextService, new TestContextService()); - instantiationService.stub(IConfigurationService, { onDidUpdateConfiguration: () => { }, onDidChangeConfiguration: () => { }, getConfiguration: () => ({}) }); + instantiationService.stub(IConfigurationService, { + onDidUpdateConfiguration: () => { }, + onDidChangeConfiguration: () => { }, + getConfiguration: () => ({}), + getValue: (key) => { + return (key === AutoCheckUpdatesConfigurationKey || key === AutoUpdateConfigurationKey) ? true : undefined; + } + }); instantiationService.stub(IExtensionManagementService, ExtensionManagementService); instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); @@ -75,20 +83,17 @@ suite('ExtensionsWorkbenchService Test', () => { instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - instantiationService.stub(IExtensionTipsService, ExtensionTipsService); - instantiationService.stub(IExtensionTipsService, 'getKeymapRecommendations', () => []); + instantiationService.set(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); instantiationService.stub(INotificationService, { prompt: () => null }); - instantiationService.stub(IDialogService, { show: () => TPromise.as(0) }); }); - setup(() => { + setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); - instantiationService.stub(IDialogService, { show: () => TPromise.as(0) }); instantiationService.stubPromise(INotificationService, 'prompt', 0); - (instantiationService.get(IExtensionEnablementService)).reset(); + await (instantiationService.get(IExtensionEnablementService)).reset(); }); teardown(() => { @@ -165,7 +170,7 @@ suite('ExtensionsWorkbenchService Test', () => { type: LocalExtensionType.User, readmeUrl: 'localReadmeUrl1', changelogUrl: 'localChangelogUrl1', - path: 'localPath1' + location: URI.file('localPath1') }); const expected2 = aLocalExtension('local2', { publisher: 'localPublisher2', @@ -211,7 +216,7 @@ suite('ExtensionsWorkbenchService Test', () => { assert.equal('1.2.0', actual.version); assert.equal('1.2.0', actual.latestVersion); assert.equal('localDescription2', actual.description); - assert.ok(fs.existsSync(actual.iconUrl)); + assert.ok(fs.existsSync(URI.parse(actual.iconUrl).fsPath)); assert.equal(null, actual.licenseUrl); assert.equal(ExtensionState.Installed, actual.state); assert.equal(null, actual.installCount); @@ -233,7 +238,7 @@ suite('ExtensionsWorkbenchService Test', () => { type: LocalExtensionType.User, readmeUrl: 'localReadmeUrl1', changelogUrl: 'localChangelogUrl1', - path: 'localPath1' + location: URI.file('localPath1') }); const local2 = aLocalExtension('local2', { publisher: 'localPublisher2', @@ -304,7 +309,7 @@ suite('ExtensionsWorkbenchService Test', () => { assert.equal('1.2.0', actual.version); assert.equal('1.2.0', actual.latestVersion); assert.equal('localDescription2', actual.description); - assert.ok(fs.existsSync(actual.iconUrl)); + assert.ok(fs.existsSync(URI.parse(actual.iconUrl).fsPath)); assert.equal(null, actual.licenseUrl); assert.equal(ExtensionState.Installed, actual.state); assert.equal(null, actual.installCount); @@ -470,7 +475,7 @@ suite('ExtensionsWorkbenchService Test', () => { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a'))); return testObject.queryGallery().then(page => { - return testObject.loadDependencies(page.firstPage[0]).then(dependencies => { + return testObject.loadDependencies(page.firstPage[0], CancellationToken.None).then(dependencies => { assert.equal(null, dependencies); }); }); @@ -483,7 +488,7 @@ suite('ExtensionsWorkbenchService Test', () => { return testObject.queryGallery().then(page => { const extension = page.firstPage[0]; - return testObject.loadDependencies(extension).then(actual => { + return testObject.loadDependencies(extension, CancellationToken.None).then(actual => { assert.ok(actual.hasDependencies); assert.equal(extension, actual.extension); assert.equal(null, actual.dependent); @@ -522,7 +527,7 @@ suite('ExtensionsWorkbenchService Test', () => { return testObject.queryGallery().then(page => { const extension = page.firstPage[0]; - return testObject.loadDependencies(extension).then(actual => { + return testObject.loadDependencies(extension, CancellationToken.None).then(actual => { assert.ok(actual.hasDependencies); assert.equal(extension, actual.extension); assert.equal(null, actual.dependent); @@ -554,7 +559,7 @@ suite('ExtensionsWorkbenchService Test', () => { return testObject.queryGallery().then(page => { const extension = page.firstPage[0]; - return testObject.loadDependencies(extension).then(actual => { + return testObject.loadDependencies(extension, CancellationToken.None).then(actual => { assert.ok(actual.hasDependencies); assert.equal(extension, actual.extension); assert.equal(null, actual.dependent); @@ -588,7 +593,7 @@ suite('ExtensionsWorkbenchService Test', () => { return testObject.queryGallery().then(page => { const extension = page.firstPage[0]; - return testObject.loadDependencies(extension).then(actual => { + return testObject.loadDependencies(extension, CancellationToken.None).then(actual => { assert.ok(actual.hasDependencies); assert.equal(extension, actual.extension); assert.equal(null, actual.dependent); @@ -626,7 +631,7 @@ suite('ExtensionsWorkbenchService Test', () => { return testObject.queryGallery().then(page => { const extension = page.firstPage[0]; - return testObject.loadDependencies(extension).then(a => { + return testObject.loadDependencies(extension, CancellationToken.None).then(a => { assert.ok(a.hasDependencies); assert.equal(extension, a.extension); assert.equal(null, a.dependent); @@ -840,7 +845,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 1); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) @@ -851,8 +855,28 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); - test('test disable extension with dependencies disable all', () => { - const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); + test('test disable extension pack disables the pack', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + + return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + + test('test disable extension pack disable all', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); const extensionB = aLocalExtension('b'); const extensionC = aLocalExtension('c'); @@ -861,7 +885,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) @@ -887,6 +910,24 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); + test('test disable extension when extension is part of a pack', () => { + const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] }); + const extensionB = aLocalExtension('b'); + const extensionC = aLocalExtension('c'); + + return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) + .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) + .then(() => { + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); + testObject = instantiationService.createInstance(ExtensionsWorkbenchService); + return testObject.setEnablement(testObject.local[1], EnablementState.Disabled) + .then(() => { + assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); + }); + }); + }); + test('test disable both dependency and dependent do not promot and do not fail', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -898,7 +939,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); const target = sinon.spy(); - instantiationService.stub(IDialogService, 'show', Promise.resolve().then(target)); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement([testObject.local[1], testObject.local[0]], EnablementState.Disabled) @@ -921,7 +961,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); const target = sinon.spy(); - instantiationService.stub(IDialogService, 'show', Promise.resolve().then(target)); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement([testObject.local[1], testObject.local[0]], EnablementState.Enabled) @@ -943,7 +982,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 1); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) @@ -953,22 +991,6 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); - test('test disable extension fails if its dependency is a dependent of other', () => { - const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); - const extensionB = aLocalExtension('b'); - const extensionC = aLocalExtension('c', { extensionDependencies: ['pub.b'] }); - - return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Enabled) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Enabled)) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) - .then(() => { - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); - return testObject.setEnablement(testObject.local[0], EnablementState.Disabled).then(() => assert.fail('Should fail'), error => assert.ok(true)); - }); - }); - test('test disable extension if its dependency is a dependent of other disabled extension', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -979,14 +1001,11 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Disabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) .then(() => { assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); }); }); }); @@ -1001,15 +1020,10 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) - .then(() => { - assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - }); + .then(() => assert.fail('An extension with dependent should not be disabled'), () => null); }); }); @@ -1023,7 +1037,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); @@ -1042,16 +1055,9 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Enabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); - return testObject.setEnablement(testObject.local[0], EnablementState.Disabled) - .then(() => { - assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - }); + .then(() => assert.fail('An extension with dependent should not be disabled'), () => null); }); }); @@ -1065,7 +1071,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Disabled)) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 0); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Enabled) @@ -1076,27 +1081,6 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); - test('test enable extension with dependencies does not enable if cancelled', () => { - const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); - const extensionB = aLocalExtension('b'); - const extensionC = aLocalExtension('c'); - - return instantiationService.get(IExtensionEnablementService).setEnablement(extensionA, EnablementState.Disabled) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionB, EnablementState.Disabled)) - .then(() => instantiationService.get(IExtensionEnablementService).setEnablement(extensionC, EnablementState.Disabled)) - .then(() => { - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); - instantiationService.stubPromise(IDialogService, 'show', 1); - testObject = instantiationService.createInstance(ExtensionsWorkbenchService); - - return testObject.setEnablement(testObject.local[0], EnablementState.Enabled) - .then(() => { - assert.equal(testObject.local[0].enablementState, EnablementState.Disabled); - assert.equal(testObject.local[1].enablementState, EnablementState.Disabled); - }); - }); - }); - test('test enable extension with dependencies does not prompt if dependency is enabled already', () => { const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] }); const extensionB = aLocalExtension('b'); @@ -1108,7 +1092,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); const target = sinon.spy(); - instantiationService.stubPromise(IDialogService, 'show', Promise.resolve().then(target)); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement(testObject.local[0], EnablementState.Enabled) @@ -1130,7 +1113,6 @@ suite('ExtensionsWorkbenchService Test', () => { .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]); const target = sinon.spy(); - instantiationService.stubPromise(IDialogService, 'show', Promise.resolve().then(target)); testObject = instantiationService.createInstance(ExtensionsWorkbenchService); return testObject.setEnablement([testObject.local[1], testObject.local[0]], EnablementState.Enabled) @@ -1199,6 +1181,7 @@ suite('ExtensionsWorkbenchService Test', () => { assign(localExtension.manifest, { name, publisher: 'pub', version: '1.0.0' }, manifest); localExtension.identifier = { id: getLocalExtensionIdFromManifest(localExtension.manifest) }; localExtension.metadata = { id: localExtension.identifier.id, publisherId: localExtension.manifest.publisher, publisherDisplayName: 'somename' }; + localExtension.galleryIdentifier = { id: getGalleryExtensionIdFromLocal(localExtension), uuid: void 0 }; return localExtension; } @@ -1236,4 +1219,4 @@ suite('ExtensionsWorkbenchService Test', () => { }); }); } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedback.contribution.ts b/src/vs/workbench/parts/feedback/electron-browser/feedback.contribution.ts index 9f8b1102649..98e0bb8bfa2 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedback.contribution.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedback.contribution.ts @@ -5,7 +5,8 @@ 'use strict'; import { Registry } from 'vs/platform/registry/common/platform'; -import { StatusbarAlignment, IStatusbarRegistry, Extensions, StatusbarItemDescriptor } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, Extensions, StatusbarItemDescriptor } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { FeedbackStatusbarItem } from 'vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem'; import { localize } from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts index eb1e969e4a8..41d9127b72b 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedback.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedback.ts @@ -7,14 +7,12 @@ import 'vs/css!./media/feedback'; import * as nls from 'vs/nls'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Builder, $ } from 'vs/base/browser/builder'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { Dropdown } from 'vs/base/browser/ui/dropdown/dropdown'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import product from 'vs/platform/node/product'; import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import * as errors from 'vs/base/common/errors'; import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; @@ -31,45 +29,37 @@ export interface IFeedback { sentiment: number; } -export interface IFeedbackService { +export interface IFeedbackDelegate { submitFeedback(feedback: IFeedback): void; getCharacterLimit(sentiment: number): number; } export interface IFeedbackDropdownOptions { contextViewProvider: IContextViewService; - feedbackService?: IFeedbackService; + feedbackService?: IFeedbackDelegate; onFeedbackVisibilityChange?: (visible: boolean) => void; } -enum FormEvent { - SENDING, - SENT, - SEND_ERROR -} - export class FeedbackDropdown extends Dropdown { private maxFeedbackCharacters: number; - private feedback: string; - private sentiment: number; - private isSendingFeedback: boolean; + private feedback: string = ''; + private sentiment: number = 1; private autoHideTimeout: number; - private feedbackService: IFeedbackService; + private feedbackDelegate: IFeedbackDelegate; private feedbackForm: HTMLFormElement; private feedbackDescriptionInput: HTMLTextAreaElement; - private smileyInput: Builder; - private frownyInput: Builder; + private smileyInput: HTMLElement; + private frownyInput: HTMLElement; private sendButton: Button; - private $sendButton: Builder; private hideButton: HTMLInputElement; - private remainingCharacterCount: Builder; + private remainingCharacterCount: HTMLElement; private requestFeatureLink: string; - private _isPure: boolean; + private isPure: boolean = true; constructor( container: HTMLElement, @@ -83,64 +73,56 @@ export class FeedbackDropdown extends Dropdown { super(container, { contextViewProvider: options.contextViewProvider, labelRenderer: (container: HTMLElement): IDisposable => { - $(container).addClass('send-feedback', 'mask-icon'); + dom.addClasses(container, 'send-feedback', 'mask-icon'); - return null; + return Disposable.None; } }); - this._isPure = true; + this.feedbackDelegate = options.feedbackService; + this.maxFeedbackCharacters = this.feedbackDelegate.getCharacterLimit(this.sentiment); + this.requestFeatureLink = product.sendASmile.requestFeatureUrl; + this.integrityService.isPure().then(result => { if (!result.isPure) { - this._isPure = false; + this.isPure = false; } }); dom.addClass(this.element, 'send-feedback'); this.element.title = nls.localize('sendFeedback', "Tweet Feedback"); - - this.feedbackService = options.feedbackService; - - this.feedback = ''; - this.sentiment = 1; - this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment); - - this.feedbackForm = null; - this.feedbackDescriptionInput = null; - - this.smileyInput = null; - this.frownyInput = null; - - this.sendButton = null; - this.$sendButton = null; - - this.requestFeatureLink = product.sendASmile.requestFeatureUrl; } protected getAnchor(): HTMLElement | IAnchor { - const res = dom.getDomNodePagePosition(this.element); + const position = dom.getDomNodePagePosition(this.element); return { - x: res.left, - y: res.top - 9, /* above the status bar */ - width: res.width, - height: res.height + x: position.left + position.width, // center above the container + y: position.top - 9, // above status bar + width: position.width, + height: position.height } as IAnchor; } protected renderContents(container: HTMLElement): IDisposable { - const $form = $('form.feedback-form').attr({ - action: 'javascript:void(0);' - }).appendTo(container); + const disposables: IDisposable[] = []; - $(container).addClass('monaco-menu-container'); + dom.addClass(container, 'monaco-menu-container'); - this.feedbackForm = $form.getHTMLElement(); + // Form + this.feedbackForm = dom.append(container, dom.$('form.feedback-form')); + this.feedbackForm.setAttribute('action', 'javascript:void(0);'); - $('h2.title').text(nls.localize("label.sendASmile", "Tweet us your feedback.")).appendTo($form); + // Title + dom.append(this.feedbackForm, dom.$('h2.title')).textContent = nls.localize("label.sendASmile", "Tweet us your feedback."); - const cancelBtn = $('div.cancel').attr('tabindex', '0'); - cancelBtn.on(dom.EventType.MOUSE_OVER, () => { + // Close Button (top right) + const closeBtn = dom.append(this.feedbackForm, dom.$('div.cancel')); + closeBtn.tabIndex = 0; + closeBtn.setAttribute('role', 'button'); + closeBtn.title = nls.localize('close', "Close"); + + disposables.push(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OVER, () => { const theme = this.themeService.getTheme(); let darkenFactor: number; switch (theme.type) { @@ -153,119 +135,153 @@ export class FeedbackDropdown extends Dropdown { } if (darkenFactor) { - cancelBtn.getHTMLElement().style.backgroundColor = darken(theme.getColor(editorWidgetBackground), darkenFactor)(theme).toString(); + closeBtn.style.backgroundColor = darken(theme.getColor(editorWidgetBackground), darkenFactor)(theme).toString(); } - }); - cancelBtn.on(dom.EventType.MOUSE_OUT, () => { - cancelBtn.getHTMLElement().style.backgroundColor = null; - }); - this.invoke(cancelBtn, () => { - this.hide(); - }).appendTo($form); + })); - const $content = $('div.content').appendTo($form); + disposables.push(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OUT, () => { + closeBtn.style.backgroundColor = null; + })); - const $sentimentContainer = $('div').appendTo($content); - if (!this._isPure) { - $('span').text(nls.localize("patchedVersion1", "Your installation is corrupt.")).appendTo($sentimentContainer); - $('br').appendTo($sentimentContainer); - $('span').text(nls.localize("patchedVersion2", "Please specify this if you submit a bug.")).appendTo($sentimentContainer); - $('br').appendTo($sentimentContainer); + this.invoke(closeBtn, disposables, () => this.hide()); + + // Content + const content = dom.append(this.feedbackForm, dom.$('div.content')); + + // Sentiment Buttons + const sentimentContainer = dom.append(content, dom.$('div')); + + if (!this.isPure) { + dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion1", "Your installation is corrupt."); + sentimentContainer.appendChild(document.createElement('br')); + dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion2", "Please specify this if you submit a bug."); + sentimentContainer.appendChild(document.createElement('br')); } - $('span').text(nls.localize("sentiment", "How was your experience?")).appendTo($sentimentContainer); - const $feedbackSentiment = $('div.feedback-sentiment').appendTo($sentimentContainer); + dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("sentiment", "How was your experience?"); - this.smileyInput = $('div').addClass('sentiment smile').attr({ - 'aria-checked': 'false', - 'aria-label': nls.localize('smileCaption', "Happy"), - 'tabindex': 0, - 'role': 'checkbox' - }); - this.invoke(this.smileyInput, () => { this.setSentiment(true); }).appendTo($feedbackSentiment); + const feedbackSentiment = dom.append(sentimentContainer, dom.$('div.feedback-sentiment')); - this.frownyInput = $('div').addClass('sentiment frown').attr({ - 'aria-checked': 'false', - 'aria-label': nls.localize('frownCaption', "Sad"), - 'tabindex': 0, - 'role': 'checkbox' - }); + // Sentiment: Smiley + this.smileyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + dom.addClass(this.smileyInput, 'smile'); + this.smileyInput.setAttribute('aria-checked', 'false'); + this.smileyInput.setAttribute('aria-label', nls.localize('smileCaption', "Happy Feedback Sentiment")); + this.smileyInput.setAttribute('role', 'checkbox'); + this.smileyInput.title = nls.localize('smileCaption', "Happy Feedback Sentiment"); + this.smileyInput.tabIndex = 0; - this.invoke(this.frownyInput, () => { this.setSentiment(false); }).appendTo($feedbackSentiment); + this.invoke(this.smileyInput, disposables, () => this.setSentiment(true)); + + // Sentiment: Frowny + this.frownyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + dom.addClass(this.frownyInput, 'frown'); + this.frownyInput.setAttribute('aria-checked', 'false'); + this.frownyInput.setAttribute('aria-label', nls.localize('frownCaption', "Sad Feedback Sentiment")); + this.frownyInput.setAttribute('role', 'checkbox'); + this.frownyInput.title = nls.localize('frownCaption', "Sad Feedback Sentiment"); + this.frownyInput.tabIndex = 0; + + this.invoke(this.frownyInput, disposables, () => this.setSentiment(false)); if (this.sentiment === 1) { - this.smileyInput.addClass('checked').attr('aria-checked', 'true'); + dom.addClass(this.smileyInput, 'checked'); + this.smileyInput.setAttribute('aria-checked', 'true'); } else { - this.frownyInput.addClass('checked').attr('aria-checked', 'true'); + dom.addClass(this.frownyInput, 'checked'); + this.frownyInput.setAttribute('aria-checked', 'true'); } - const $contactUs = $('div.contactus').appendTo($content); + // Contact Us Box + const contactUsContainer = dom.append(content, dom.$('div.contactus')); - $('span').text(nls.localize("other ways to contact us", "Other ways to contact us")).appendTo($contactUs); + dom.append(contactUsContainer, dom.$('span')).textContent = nls.localize("other ways to contact us", "Other ways to contact us"); - const $contactUsContainer = $('div.channels').appendTo($contactUs); + const channelsContainer = dom.append(contactUsContainer, dom.$('div.channels')); - $('div').append($('a').attr('target', '_blank').attr('href', '#').text(nls.localize("submit a bug", "Submit a bug")).attr('tabindex', '0')) - .on('click', event => { - dom.EventHelper.stop(event); - const actionId = 'workbench.action.openIssueReporter'; - this.commandService.executeCommand(actionId).done(null, errors.onUnexpectedError); + // Contact: Submit a Bug + const submitBugLinkContainer = dom.append(channelsContainer, dom.$('div')); - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('workbenchActionExecuted', { id: actionId, from: 'feedback' }); - }) - .appendTo($contactUsContainer); + const submitBugLink = dom.append(submitBugLinkContainer, dom.$('a')); + submitBugLink.setAttribute('target', '_blank'); + submitBugLink.setAttribute('href', '#'); + submitBugLink.textContent = nls.localize("submit a bug", "Submit a bug"); + submitBugLink.tabIndex = 0; - $('div').append($('a').attr('target', '_blank').attr('href', this.requestFeatureLink).text(nls.localize("request a missing feature", "Request a missing feature")).attr('tabindex', '0')) - .appendTo($contactUsContainer); + disposables.push(dom.addDisposableListener(submitBugLink, 'click', e => { + dom.EventHelper.stop(event); + const actionId = 'workbench.action.openIssueReporter'; + this.commandService.executeCommand(actionId); + this.hide(); - this.remainingCharacterCount = $('span.char-counter').text(this.getCharCountText(0)); + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id: actionId, from: 'feedback' }); + })); - $('h3').text(nls.localize("tell us why?", "Tell us why?")) - .append(this.remainingCharacterCount) - .appendTo($form); + // Contact: Request a Feature + if (!!this.requestFeatureLink) { + const requestFeatureLinkContainer = dom.append(channelsContainer, dom.$('div')); - this.feedbackDescriptionInput = $('textarea.feedback-description').attr({ - rows: 3, - maxlength: this.maxFeedbackCharacters, - 'aria-label': nls.localize("commentsHeader", "Comments") - }) - .text(this.feedback).attr('required', 'required') - .on('keyup', () => { - this.updateCharCountText(); - }) - .appendTo($form).domFocus().getHTMLElement(); + const requestFeatureLink = dom.append(requestFeatureLinkContainer, dom.$('a')); + requestFeatureLink.setAttribute('target', '_blank'); + requestFeatureLink.setAttribute('href', this.requestFeatureLink); + requestFeatureLink.textContent = nls.localize("request a missing feature", "Request a missing feature"); + requestFeatureLink.tabIndex = 0; - const $buttons = $('div.form-buttons').appendTo($form); + disposables.push(dom.addDisposableListener(requestFeatureLink, 'click', e => this.hide())); + } - const $hideButtonContainer = $('div.hide-button-container').appendTo($buttons); + // Remaining Characters + const remainingCharacterCountContainer = dom.append(this.feedbackForm, dom.$('h3')); + remainingCharacterCountContainer.textContent = nls.localize("tell us why?", "Tell us why?"); - this.hideButton = $('input.hide-button').type('checkbox').attr('checked', '').id('hide-button').appendTo($hideButtonContainer).getHTMLElement() as HTMLInputElement; + this.remainingCharacterCount = dom.append(remainingCharacterCountContainer, dom.$('span.char-counter')); + this.remainingCharacterCount.textContent = this.getCharCountText(0); - $('label').attr('for', 'hide-button').text(nls.localize('showFeedback', "Show Feedback Smiley in Status Bar")).appendTo($hideButtonContainer); + // Feedback Input Form + this.feedbackDescriptionInput = dom.append(this.feedbackForm, dom.$('textarea.feedback-description')); + this.feedbackDescriptionInput.rows = 3; + this.feedbackDescriptionInput.maxLength = this.maxFeedbackCharacters; + this.feedbackDescriptionInput.textContent = this.feedback; + this.feedbackDescriptionInput.required = true; + this.feedbackDescriptionInput.setAttribute('aria-label', nls.localize("feedbackTextInput", "Tell us your feedback")); + this.feedbackDescriptionInput.focus(); - this.sendButton = new Button($buttons.getHTMLElement()); + disposables.push(dom.addDisposableListener(this.feedbackDescriptionInput, 'keyup', () => this.updateCharCountText())); + + // Feedback Input Form Buttons Container + const buttonsContainer = dom.append(this.feedbackForm, dom.$('div.form-buttons')); + + // Checkbox: Hide Feedback Smiley + const hideButtonContainer = dom.append(buttonsContainer, dom.$('div.hide-button-container')); + + this.hideButton = dom.append(hideButtonContainer, dom.$('input.hide-button')); + this.hideButton.type = 'checkbox'; + this.hideButton.checked = true; + this.hideButton.id = 'hide-button'; + + const hideButtonLabel = dom.append(hideButtonContainer, dom.$('label')); + hideButtonLabel.setAttribute('for', 'hide-button'); + hideButtonLabel.textContent = nls.localize('showFeedback', "Show Feedback Smiley in Status Bar"); + + // Button: Send Feedback + this.sendButton = new Button(buttonsContainer); this.sendButton.enabled = false; this.sendButton.label = nls.localize('tweet', "Tweet"); - this.$sendButton = new Builder(this.sendButton.element); - this.$sendButton.addClass('send'); - this.toDispose.push(attachButtonStyler(this.sendButton, this.themeService)); + dom.addClass(this.sendButton.element, 'send'); + this.sendButton.element.title = nls.localize('tweetFeedback', "Tweet Feedback"); + disposables.push(attachButtonStyler(this.sendButton, this.themeService)); - this.invoke(this.$sendButton, () => { - if (this.isSendingFeedback) { - return; - } - this.onSubmit(); - }); + this.sendButton.onDidClick(() => this.onSubmit()); - this.toDispose.push(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground, inputBackground, inputForeground, inputBorder, editorBackground, contrastBorder }, colors => { - $form.style('background-color', colors.editorWidgetBackground ? colors.editorWidgetBackground.toString() : null); - $form.style('box-shadow', colors.widgetShadow ? `0 0 8px ${colors.widgetShadow}` : null); + disposables.push(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground, inputBackground, inputForeground, inputBorder, editorBackground, contrastBorder }, colors => { + this.feedbackForm.style.backgroundColor = colors.editorWidgetBackground ? colors.editorWidgetBackground.toString() : null; + this.feedbackForm.style.boxShadow = colors.widgetShadow ? `0 0 8px ${colors.widgetShadow}` : null; if (this.feedbackDescriptionInput) { this.feedbackDescriptionInput.style.backgroundColor = colors.inputBackground ? colors.inputBackground.toString() : null; @@ -273,8 +289,8 @@ export class FeedbackDropdown extends Dropdown { this.feedbackDescriptionInput.style.border = `1px solid ${colors.inputBorder || 'transparent'}`; } - $contactUs.style('background-color', colors.editorBackground ? colors.editorBackground.toString() : null); - $contactUs.style('border', `1px solid ${colors.contrastBorder || 'transparent'}`); + contactUsContainer.style.backgroundColor = colors.editorBackground ? colors.editorBackground.toString() : null; + contactUsContainer.style.border = `1px solid ${colors.contrastBorder || 'transparent'}`; })); return { @@ -283,6 +299,8 @@ export class FeedbackDropdown extends Dropdown { this.feedbackDescriptionInput = null; this.smileyInput = null; this.frownyInput = null; + + dispose(disposables); } }; } @@ -293,49 +311,49 @@ export class FeedbackDropdown extends Dropdown { ? nls.localize("character left", "character left") : nls.localize("characters left", "characters left"); - return '(' + remaining + ' ' + text + ')'; + return `(${remaining} ${text})`; } private updateCharCountText(): void { - this.remainingCharacterCount.text(this.getCharCountText(this.feedbackDescriptionInput.value.length)); + this.remainingCharacterCount.innerText = this.getCharCountText(this.feedbackDescriptionInput.value.length); this.sendButton.enabled = this.feedbackDescriptionInput.value.length > 0; } private setSentiment(smile: boolean): void { if (smile) { - this.smileyInput.addClass('checked'); - this.smileyInput.attr('aria-checked', 'true'); - this.frownyInput.removeClass('checked'); - this.frownyInput.attr('aria-checked', 'false'); + dom.addClass(this.smileyInput, 'checked'); + this.smileyInput.setAttribute('aria-checked', 'true'); + dom.removeClass(this.frownyInput, 'checked'); + this.frownyInput.setAttribute('aria-checked', 'false'); } else { - this.frownyInput.addClass('checked'); - this.frownyInput.attr('aria-checked', 'true'); - this.smileyInput.removeClass('checked'); - this.smileyInput.attr('aria-checked', 'false'); + dom.addClass(this.frownyInput, 'checked'); + this.frownyInput.setAttribute('aria-checked', 'true'); + dom.removeClass(this.smileyInput, 'checked'); + this.smileyInput.setAttribute('aria-checked', 'false'); } this.sentiment = smile ? 1 : 0; - this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment); + this.maxFeedbackCharacters = this.feedbackDelegate.getCharacterLimit(this.sentiment); this.updateCharCountText(); - $(this.feedbackDescriptionInput).attr({ maxlength: this.maxFeedbackCharacters }); + this.feedbackDescriptionInput.maxLength = this.maxFeedbackCharacters; } - private invoke(element: Builder, callback: () => void): Builder { - element.on('click', callback); + private invoke(element: HTMLElement, disposables: IDisposable[], callback: () => void): HTMLElement { + disposables.push(dom.addDisposableListener(element, 'click', callback)); - element.on('keypress', (e) => { + disposables.push(dom.addDisposableListener(element, 'keypress', e => { if (e instanceof KeyboardEvent) { const keyboardEvent = e; if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) { // Enter or Spacebar callback(); } } - }); + })); return element; } - public show(): void { + show(): void { super.show(); if (this.options.onFeedbackVisibilityChange) { @@ -349,7 +367,7 @@ export class FeedbackDropdown extends Dropdown { } } - public hide(): void { + hide(): void { if (this.feedbackDescriptionInput) { this.feedback = this.feedbackDescriptionInput.value; } @@ -360,13 +378,13 @@ export class FeedbackDropdown extends Dropdown { } if (this.hideButton && !this.hideButton.checked) { - this.configurationService.updateValue(FEEDBACK_VISIBLE_CONFIG, false).done(null, errors.onUnexpectedError); + this.configurationService.updateValue(FEEDBACK_VISIBLE_CONFIG, false); } super.hide(); } - public onEvent(e: Event, activeElement: HTMLElement): void { + onEvent(e: Event, activeElement: HTMLElement): void { if (e instanceof KeyboardEvent) { const keyboardEvent = e; if (keyboardEvent.keyCode === 27) { // Escape @@ -380,54 +398,12 @@ export class FeedbackDropdown extends Dropdown { return; } - this.changeFormStatus(FormEvent.SENDING); - - this.feedbackService.submitFeedback({ + this.feedbackDelegate.submitFeedback({ feedback: this.feedbackDescriptionInput.value, sentiment: this.sentiment }); - this.changeFormStatus(FormEvent.SENT); - } - - - private changeFormStatus(event: FormEvent): void { - switch (event) { - case FormEvent.SENDING: - this.isSendingFeedback = true; - this.sendButton.label = nls.localize('feedbackSending', "Sending"); - this.$sendButton.addClass('in-progress'); - break; - case FormEvent.SENT: - this.isSendingFeedback = false; - this.sendButton.label = nls.localize('feedbackSent', "Thanks"); - this.$sendButton.addClass('success'); - this.resetForm(); - this.autoHideTimeout = setTimeout(() => { - this.hide(); - }, 1000); - this.$sendButton.off(['click', 'keypress']); - this.invoke(this.$sendButton, () => { - this.hide(); - this.$sendButton.off(['click', 'keypress']); - this.$sendButton.removeClass('in-progress'); - }); - break; - case FormEvent.SEND_ERROR: - this.isSendingFeedback = false; - this.$sendButton.addClass('error'); - this.sendButton.label = nls.localize('feedbackSendingError', "Try again"); - break; - } - } - - private resetForm(): void { - if (this.feedbackDescriptionInput) { - this.feedbackDescriptionInput.value = ''; - } - - this.sentiment = 1; - this.maxFeedbackCharacters = this.feedbackService.getCharacterLimit(this.sentiment); + this.hide(); } } diff --git a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts index 1a659b36d9a..fb2b6026426 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/parts/feedback/electron-browser/feedbackStatusbarItem.ts @@ -7,7 +7,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { FeedbackDropdown, IFeedback, IFeedbackService, FEEDBACK_VISIBLE_CONFIG, IFeedbackDropdownOptions } from 'vs/workbench/parts/feedback/electron-browser/feedback'; +import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG, IFeedbackDropdownOptions } from 'vs/workbench/parts/feedback/electron-browser/feedback'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/node/product'; @@ -16,13 +16,12 @@ import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { clearNode, EventHelper, addClass, removeClass } from 'vs/base/browser/dom'; -import { $ } from 'vs/base/browser/builder'; +import { clearNode, EventHelper, addClass, removeClass, addDisposableListener } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; -class TwitterFeedbackService implements IFeedbackService { +class TwitterFeedbackService implements IFeedbackDelegate { private static TWITTER_URL: string = 'https://twitter.com/intent/tweet'; private static VIA_NAME: string = 'code'; @@ -32,14 +31,14 @@ class TwitterFeedbackService implements IFeedbackService { return TwitterFeedbackService.HASHTAGS.join(','); } - public submitFeedback(feedback: IFeedback): void { + submitFeedback(feedback: IFeedback): void { const queryString = `?${feedback.sentiment === 1 ? `hashtags=${this.combineHashTagsAsString()}&` : null}ref_src=twsrc%5Etfw&related=twitterapi%2Ctwitter&text=${encodeURIComponent(feedback.feedback)}&tw_p=tweetbutton&via=${TwitterFeedbackService.VIA_NAME}`; const url = TwitterFeedbackService.TWITTER_URL + queryString; window.open(url); } - public getCharacterLimit(sentiment: number): number { + getCharacterLimit(sentiment: number): number { let length: number = 0; if (sentiment === 1) { TwitterFeedbackService.HASHTAGS.forEach(element => { @@ -73,15 +72,14 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { this.enabled = this.configurationService.getValue(FEEDBACK_VISIBLE_CONFIG); - this.hideAction = this.instantiationService.createInstance(HideAction); - this.toUnbind.push(this.hideAction); + this.hideAction = this._register(this.instantiationService.createInstance(HideAction)); this.registerListeners(); } private registerListeners(): void { - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); } private onConfigurationUpdated(event: IConfigurationChangeEvent): void { @@ -95,29 +93,29 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { super.updateStyles(); if (this.dropdown) { - $(this.dropdown.label).style('background-color', this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); + this.dropdown.label.style.backgroundColor = (this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)); } } - public render(element: HTMLElement): IDisposable { + render(element: HTMLElement): IDisposable { this.container = element; // Prevent showing dropdown on anything but left click - $(this.container).on('mousedown', (e: MouseEvent) => { + this.toDispose.push(addDisposableListener(this.container, 'mousedown', (e: MouseEvent) => { if (e.button !== 0) { EventHelper.stop(e, true); } - }, this.toUnbind, true); + }, true)); // Offer context menu to hide status bar entry - $(this.container).on('contextmenu', e => { + this.toDispose.push(addDisposableListener(this.container, 'contextmenu', e => { EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ getAnchor: () => this.container, getActions: () => TPromise.as([this.hideAction]) }); - }, this.toUnbind); + })); return this.update(); } @@ -128,7 +126,7 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { // Create if (enabled) { if (!this.dropdown) { - this.dropdown = this.instantiationService.createInstance(FeedbackDropdown, this.container, { + this.dropdown = this._register(this.instantiationService.createInstance(FeedbackDropdown, this.container, { contextViewProvider: this.contextViewService, feedbackService: this.instantiationService.createInstance(TwitterFeedbackService), onFeedbackVisibilityChange: visible => { @@ -138,8 +136,7 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { removeClass(this.container, 'has-beak'); } } - } as IFeedbackDropdownOptions); - this.toUnbind.push(this.dropdown); + } as IFeedbackDropdownOptions)); this.updateStyles(); @@ -166,7 +163,7 @@ class HideAction extends Action { super('feedback.hide', localize('hide', "Hide")); } - public run(extensionId: string): TPromise { + run(extensionId: string): TPromise { return this.configurationService.updateValue(FEEDBACK_VISIBLE_CONFIG, false); } } 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 3a390da8f70..3c69f00d995 100644 --- a/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css +++ b/src/vs/workbench/parts/feedback/electron-browser/media/feedback.css @@ -48,7 +48,6 @@ padding-left: 3px; } -/* TODO @C5 review link color */ .monaco-shell .feedback-form .content .channels a { padding: 2px 0; } diff --git a/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts index de2b5da49ee..bd959156029 100644 --- a/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/binaryFileEditor.ts @@ -11,7 +11,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { IFileService } from 'vs/platform/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic */ export class BinaryFileEditor extends BaseBinaryResourceEditor { - public static readonly ID = BINARY_FILE_EDITOR_ID; + static readonly ID = BINARY_FILE_EDITOR_ID; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -59,7 +59,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { }); } - public getTitle(): string { + getTitle(): string { return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer"); } } diff --git a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts index 5169f040911..9261392d0bf 100644 --- a/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/parts/files/browser/editors/fileEditorTracker.ts @@ -4,23 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import * as errors from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; -import * as paths from 'vs/base/common/paths'; +import { URI } from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { distinct } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isLinux } from 'vs/base/common/platform'; -import { ResourceQueue } from 'vs/base/common/async'; import { ResourceMap } from 'vs/base/common/map'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -29,12 +26,13 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ResourceQueue, timeout } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; -export class FileEditorTracker implements IWorkbenchContribution { +export class FileEditorTracker extends Disposable implements IWorkbenchContribution { protected closeOnFileDelete: boolean; - private toUnbind: IDisposable[]; private modelLoadQueue: ResourceQueue; private activeOutOfWorkspaceWatchers: ResourceMap; @@ -49,7 +47,8 @@ export class FileEditorTracker implements IWorkbenchContribution { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IWindowService private windowService: IWindowService ) { - this.toUnbind = []; + super(); + this.modelLoadQueue = new ResourceQueue(); this.activeOutOfWorkspaceWatchers = new ResourceMap(); @@ -61,29 +60,29 @@ export class FileEditorTracker implements IWorkbenchContribution { private registerListeners(): void { // Update editors from operation changes - this.toUnbind.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); + this._register(this.fileService.onAfterOperation(e => this.onFileOperation(e))); // Update editors from disk changes - this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); // Editor changing - this.toUnbind.push(this.editorService.onDidVisibleEditorsChange(() => this.handleOutOfWorkspaceWatchers())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this.handleOutOfWorkspaceWatchers())); // Update visible editors when focus is gained - this.toUnbind.push(this.windowService.onDidChangeFocus(e => this.onWindowFocusChange(e))); + this._register(this.windowService.onDidChangeFocus(e => this.onWindowFocusChange(e))); // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); // Configuration - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); } private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void { if (configuration.workbench && configuration.workbench.editor && typeof configuration.workbench.editor.closeOnFileDelete === 'boolean') { this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete; } else { - this.closeOnFileDelete = true; // default + this.closeOnFileDelete = false; // default } } @@ -125,7 +124,9 @@ export class FileEditorTracker implements IWorkbenchContribution { private onFileChanges(e: FileChangesEvent): void { // Handle updates - this.handleUpdates(e); + if (e.gotAdded() || e.gotUpdated()) { + this.handleUpdates(e); + } // Handle deletes if (e.gotDeleted()) { @@ -147,7 +148,7 @@ export class FileEditorTracker implements IWorkbenchContribution { // Do NOT close any opened editor that matches the resource path (either equal or being parent) of the // resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same // path but different casing. - if (movedTo && paths.isEqualOrParent(resource.fsPath, movedTo.fsPath, !isLinux /* ignorecase */) && resource.fsPath.indexOf(movedTo.fsPath) === 0) { + if (movedTo && resources.isEqualOrParent(resource, movedTo)) { return; } @@ -155,7 +156,7 @@ export class FileEditorTracker implements IWorkbenchContribution { if (arg1 instanceof FileChangesEvent) { matches = arg1.contains(resource, FileChangeType.DELETED); } else { - matches = paths.isEqualOrParent(resource.fsPath, arg1.fsPath, !isLinux /* ignorecase */); + matches = resources.isEqualOrParent(resource, arg1); } if (!matches) { @@ -168,14 +169,14 @@ export class FileEditorTracker implements IWorkbenchContribution { // file is really gone and not just a faulty file event. // This only applies to external file events, so we need to check for the isExternal // flag. - let checkExists: TPromise; + let checkExists: Thenable; if (isExternal) { - checkExists = TPromise.timeout(100).then(() => this.fileService.existsFile(resource)); + checkExists = timeout(100).then(() => this.fileService.existsFile(resource)); } else { - checkExists = TPromise.as(false); + checkExists = Promise.resolve(false); } - checkExists.done(exists => { + checkExists.then(exists => { if (!exists && !editor.isDisposed()) { editor.dispose(); } else if (this.environmentService.verbose) { @@ -217,31 +218,33 @@ export class FileEditorTracker implements IWorkbenchContribution { private handleMovedFileInOpenedEditors(oldResource: URI, newResource: URI): void { this.editorGroupService.groups.forEach(group => { - group.editors.forEach(input => { - if (input instanceof FileEditorInput) { - const resource = input.getResource(); + group.editors.forEach(editor => { + if (editor instanceof FileEditorInput) { + const resource = editor.getResource(); // Update Editor if file (or any parent of the input) got renamed or moved - if (paths.isEqualOrParent(resource.fsPath, oldResource.fsPath, !isLinux /* ignorecase */)) { + if (resources.isEqualOrParent(resource, oldResource)) { let reopenFileResource: URI; if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { const index = this.getIndexOfPath(resource.path, oldResource.path); - reopenFileResource = newResource.with({ path: paths.join(newResource.path, resource.path.substr(index + oldResource.path.length + 1)) }); // parent folder got moved + reopenFileResource = resources.joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved } - // Reopen - this.editorService.openEditor({ - resource: reopenFileResource, - options: { - preserveFocus: true, - pinned: group.isPinned(input), - index: group.getIndexOfEditor(input), - inactive: !group.isActive(input), - viewState: this.getViewStateFor(oldResource, group) - } - }, group); + this.editorService.replaceEditors([{ + editor: { resource }, + replacement: { + resource: reopenFileResource, + options: { + preserveFocus: true, + pinned: group.isPinned(editor), + index: group.getIndexOfEditor(editor), + inactive: !group.isActive(editor), + viewState: this.getViewStateFor(oldResource, group) + } + }, + }], group); } } }); @@ -286,31 +289,11 @@ export class FileEditorTracker implements IWorkbenchContribution { private handleUpdates(e: FileChangesEvent): void { - // Handle updates to visible binary editors - this.handleUpdatesToVisibleBinaryEditors(e); - // Handle updates to text models this.handleUpdatesToTextModels(e); - } - private handleUpdatesToVisibleBinaryEditors(e: FileChangesEvent): void { - const editors = this.editorService.visibleControls; - editors.forEach(editor => { - const resource = toResource(editor.input, { supportSideBySide: true }); - - // Support side-by-side binary editors too - let isBinaryEditor = false; - if (editor instanceof SideBySideEditor) { - isBinaryEditor = editor.getMasterEditor().getId() === BINARY_FILE_EDITOR_ID; - } else { - isBinaryEditor = editor.getId() === BINARY_FILE_EDITOR_ID; - } - - // Binary editor that should reload from event - if (resource && isBinaryEditor && (e.contains(resource, FileChangeType.UPDATED) || e.contains(resource, FileChangeType.ADDED))) { - this.editorService.openEditor(editor.input, { forceOpen: true, preserveFocus: true }, editor.group); - } - }); + // Handle updates to visible binary editors + this.handleUpdatesToVisibleBinaryEditors(e); } private handleUpdatesToTextModels(e: FileChangesEvent): void { @@ -332,10 +315,30 @@ export class FileEditorTracker implements IWorkbenchContribution { // to have a size of 2 (1 running load and 1 queued load). const queue = this.modelLoadQueue.queueFor(model.getResource()); if (queue.size <= 1) { - queue.queue(() => model.load().then(null, errors.onUnexpectedError)); + queue.queue(() => model.load().then(null, onUnexpectedError)); } } + private handleUpdatesToVisibleBinaryEditors(e: FileChangesEvent): void { + const editors = this.editorService.visibleControls; + editors.forEach(editor => { + const resource = toResource(editor.input, { supportSideBySide: true }); + + // Support side-by-side binary editors too + let isBinaryEditor = false; + if (editor instanceof SideBySideEditor) { + isBinaryEditor = editor.getMasterEditor().getId() === BINARY_FILE_EDITOR_ID; + } else { + isBinaryEditor = editor.getId() === BINARY_FILE_EDITOR_ID; + } + + // Binary editor that should reload from event + if (resource && isBinaryEditor && (e.contains(resource, FileChangeType.UPDATED) || e.contains(resource, FileChangeType.ADDED))) { + this.editorService.openEditor(editor.input, { forceReload: true, preserveFocus: true }, editor.group); + } + }); + } + private handleOutOfWorkspaceWatchers(): void { const visibleOutOfWorkspacePaths = new ResourceMap(); this.editorService.visibleEditors.map(editorInput => { @@ -363,8 +366,8 @@ export class FileEditorTracker implements IWorkbenchContribution { }); } - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); + dispose(): void { + super.dispose(); // Dispose watchers if any this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatchFileChanges(resource)); diff --git a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index de33f2ffaeb..beec0c28fb2 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -25,10 +25,9 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -39,7 +38,7 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; */ export class TextFileEditor extends BaseTextEditor { - public static readonly ID = TEXT_FILE_EDITOR_ID; + static readonly ID = TEXT_FILE_EDITOR_ID; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -54,12 +53,13 @@ export class TextFileEditor extends BaseTextEditor { @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService textFileService: ITextFileService, @IWindowsService private windowsService: IWindowsService, - @IPreferencesService private preferencesService: IPreferencesService + @IPreferencesService private preferencesService: IPreferencesService, + @IWindowService windowService: IWindowService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService); + super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService); // Clear view state for deleted files - this.toUnbind.push(this.fileService.onFileChanges(e => this.onFilesChanged(e))); + this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); } private onFilesChanged(e: FileChangesEvent): void { @@ -69,11 +69,11 @@ export class TextFileEditor extends BaseTextEditor { } } - public getTitle(): string { + getTitle(): string { return this.input ? this.input.getName() : nls.localize('textFileEditor', "Text File Editor"); } - public get input(): FileEditorInput { + get input(): FileEditorInput { return this._input as FileEditorInput; } @@ -83,28 +83,28 @@ export class TextFileEditor extends BaseTextEditor { // React to editors closing to preserve 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. - this.toUnbind.push((group as IEditorGroupView).onWillCloseEditor(e => { + this._register((group as IEditorGroupView).onWillCloseEditor(e => { if (e.editor === this.input) { this.doSaveTextEditorViewState(this.input); } })); } - public setOptions(options: EditorOptions): void { + setOptions(options: EditorOptions): void { const textOptions = options; if (textOptions && types.isFunction(textOptions.apply)) { textOptions.apply(this.getControl(), ScrollType.Smooth); } } - public setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Thenable { // Remember view settings if input changes this.doSaveTextEditorViewState(this.input); // Set input and resolve return super.setInput(input, options, token).then(() => { - return input.resolve(true).then(resolvedModel => { + return input.resolve().then(resolvedModel => { // Check for cancellation if (token.isCancellationRequested) { @@ -146,6 +146,9 @@ export class TextFileEditor extends BaseTextEditor { if (options && types.isFunction((options).apply)) { (options).apply(textEditor, ScrollType.Immediate); } + + // Readonly flag + textEditor.updateOptions({ readOnly: textFileModel.isReadonly() }); }, error => { // In case we tried to open a file inside the text editor and the response @@ -189,11 +192,7 @@ export class TextFileEditor extends BaseTextEditor { }); }), new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), null, true, () => { - return this.preferencesService.openGlobalSettings().then(editor => { - if (editor instanceof PreferencesEditor) { - editor.focusSearch('files.maxMemoryForLargeFilesMB'); - } - }); + return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); }) ] })); @@ -217,9 +216,9 @@ export class TextFileEditor extends BaseTextEditor { // Best we can do is to reveal the folder in the explorer if (this.contextService.isInsideWorkspace(input.getResource())) { - this.viewletService.openViewlet(VIEWLET_ID, true).done(viewlet => { + this.viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { return (viewlet as IExplorerViewlet).getExplorerView().select(input.getResource(), true); - }, errors.onUnexpectedError); + }); } }); @@ -240,7 +239,7 @@ export class TextFileEditor extends BaseTextEditor { return ariaLabel; } - public clearInput(): void { + clearInput(): void { // Keep editor view state in settings to restore when coming back this.doSaveTextEditorViewState(this.input); @@ -252,7 +251,7 @@ export class TextFileEditor extends BaseTextEditor { super.clearInput(); } - public shutdown(): void { + shutdown(): void { // Save View State this.doSaveTextEditorViewState(this.input); diff --git a/src/vs/workbench/parts/files/browser/files.ts b/src/vs/workbench/parts/files/browser/files.ts index 5b1d88c8454..38b286a0f9b 100644 --- a/src/vs/workbench/parts/files/browser/files.ts +++ b/src/vs/workbench/parts/files/browser/files.ts @@ -5,7 +5,7 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; import { ExplorerItem, OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; import { toResource } from 'vs/workbench/common/editor'; @@ -52,7 +52,7 @@ export function getMultiSelectedResources(resource: URI | object, listService: I 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. - if (selection.some(s => s.toString() === mainUriStr)) { + if (selection.some(s => URI.isUri(s) && s.toString() === mainUriStr)) { return selection; } } diff --git a/src/vs/workbench/parts/files/common/dirtyFilesTracker.ts b/src/vs/workbench/parts/files/common/dirtyFilesTracker.ts index 34594b0301e..2fd4e654b07 100644 --- a/src/vs/workbench/parts/files/common/dirtyFilesTracker.ts +++ b/src/vs/workbench/parts/files/common/dirtyFilesTracker.ts @@ -12,16 +12,15 @@ import { TextFileModelChangeEvent, ITextFileService, AutoSaveMode, ModelState } import { platform, Platform } from 'vs/base/common/platform'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import * as arrays from 'vs/base/common/arrays'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -export class DirtyFilesTracker implements IWorkbenchContribution { +export class DirtyFilesTracker extends Disposable implements IWorkbenchContribution { private isDocumentedEdited: boolean; - private toUnbind: IDisposable[]; private lastDirtyCount: number; private badgeHandle: IDisposable; @@ -33,7 +32,8 @@ export class DirtyFilesTracker implements IWorkbenchContribution { @IWindowService private windowService: IWindowService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService ) { - this.toUnbind = []; + super(); + this.isDocumentedEdited = false; this.registerListeners(); @@ -42,11 +42,11 @@ export class DirtyFilesTracker implements IWorkbenchContribution { private registerListeners(): void { // Local text file changes - this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); - this.toUnbind.push(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e))); - this.toUnbind.push(this.textFileService.models.onModelsSaved(e => this.onTextFilesSaved(e))); - this.toUnbind.push(this.textFileService.models.onModelsSaveError(e => this.onTextFilesSaveError(e))); - this.toUnbind.push(this.textFileService.models.onModelsReverted(e => this.onTextFilesReverted(e))); + this._register(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); + this._register(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e))); + this._register(this.textFileService.models.onModelsSaved(e => this.onTextFilesSaved(e))); + this._register(this.textFileService.models.onModelsSaveError(e => this.onTextFilesSaveError(e))); + this._register(this.textFileService.models.onModelsReverted(e => this.onTextFilesReverted(e))); // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); @@ -142,8 +142,4 @@ export class DirtyFilesTracker implements IWorkbenchContribution { this.windowService.setDocumentEdited(hasDirtyFiles); } } - - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); - } } diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 6bf9d8ca94d..7b4e0c6d20f 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -9,21 +9,19 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { memoize } from 'vs/base/common/decorators'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; -import * as labels from 'vs/base/common/labels'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ITextFileService, AutoSaveMode, ModelState, TextFileModelChangeEvent, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; +import { IReference } from 'vs/base/common/lifecycle'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; /** * A file editor input is the input type for the file editor of file system resources. @@ -34,7 +32,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private forceOpenAsText: boolean; private textModelReference: TPromise>; private name: string; - private toUnbind: IDisposable[]; /** * An editor input who's contents are retrieved from file services. @@ -43,16 +40,13 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private resource: URI, preferredEncoding: string, @IInstantiationService private instantiationService: IInstantiationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, @ITextFileService private textFileService: ITextFileService, - @IEnvironmentService private environmentService: IEnvironmentService, @ITextModelService private textModelResolverService: ITextModelService, - @IHashService private hashService: IHashService + @IHashService private hashService: IHashService, + @ILabelService private labelService: ILabelService ) { super(); - this.toUnbind = []; - this.setPreferredEncoding(preferredEncoding); this.registerListeners(); @@ -61,11 +55,11 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private registerListeners(): void { // Model changes - this.toUnbind.push(this.textFileService.models.onModelDirty(e => this.onDirtyStateChange(e))); - this.toUnbind.push(this.textFileService.models.onModelSaveError(e => this.onDirtyStateChange(e))); - this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onDirtyStateChange(e))); - this.toUnbind.push(this.textFileService.models.onModelReverted(e => this.onDirtyStateChange(e))); - this.toUnbind.push(this.textFileService.models.onModelOrphanedChanged(e => this.onModelOrphanedChanged(e))); + this._register(this.textFileService.models.onModelDirty(e => this.onDirtyStateChange(e))); + this._register(this.textFileService.models.onModelSaveError(e => this.onDirtyStateChange(e))); + this._register(this.textFileService.models.onModelSaved(e => this.onDirtyStateChange(e))); + this._register(this.textFileService.models.onModelReverted(e => this.onDirtyStateChange(e))); + this._register(this.textFileService.models.onModelOrphanedChanged(e => this.onModelOrphanedChanged(e))); } private onDirtyStateChange(e: TextFileModelChangeEvent): void { @@ -80,11 +74,11 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - public getResource(): URI { + getResource(): URI { return this.resource; } - public setPreferredEncoding(encoding: string): void { + setPreferredEncoding(encoding: string): void { this.preferredEncoding = encoding; if (encoding) { @@ -92,7 +86,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - public getEncoding(): string { + getEncoding(): string { const textModel = this.textFileService.models.get(this.resource); if (textModel) { return textModel.getEncoding(); @@ -101,11 +95,11 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return this.preferredEncoding; } - public getPreferredEncoding(): string { + getPreferredEncoding(): string { return this.preferredEncoding; } - public setEncoding(encoding: string, mode: EncodingMode): void { + setEncoding(encoding: string, mode: EncodingMode): void { this.preferredEncoding = encoding; const textModel = this.textFileService.models.get(this.resource); @@ -114,44 +108,44 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - public setForceOpenAsText(): void { + setForceOpenAsText(): void { this.forceOpenAsText = true; this.forceOpenAsBinary = false; } - public setForceOpenAsBinary(): void { + setForceOpenAsBinary(): void { this.forceOpenAsBinary = true; this.forceOpenAsText = false; } - public getTypeId(): string { + getTypeId(): string { return FILE_EDITOR_INPUT_ID; } - public getName(): string { + getName(): string { if (!this.name) { this.name = resources.basenameOrAuthority(this.resource); } - return this.decorateOrphanedFiles(this.name); + return this.decorateLabel(this.name); } @memoize private get shortDescription(): string { - return paths.basename(labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService)); + return paths.basename(this.labelService.getUriLabel(resources.dirname(this.resource))); } @memoize private get mediumDescription(): string { - return labels.getPathLabel(resources.dirname(this.resource), this.contextService, this.environmentService); + return this.labelService.getUriLabel(resources.dirname(this.resource), true); } @memoize private get longDescription(): string { - return labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService); + return this.labelService.getUriLabel(resources.dirname(this.resource), true); } - public getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { + getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { let description: string; switch (verbosity) { case Verbosity.SHORT: @@ -176,15 +170,15 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { @memoize private get mediumTitle(): string { - return labels.getPathLabel(this.resource, this.contextService, this.environmentService); + return this.labelService.getUriLabel(this.resource, true); } @memoize private get longTitle(): string { - return labels.getPathLabel(this.resource, void 0, this.environmentService); + return this.labelService.getUriLabel(this.resource); } - public getTitle(verbosity: Verbosity): string { + getTitle(verbosity: Verbosity): string { let title: string; switch (verbosity) { case Verbosity.SHORT: @@ -198,19 +192,22 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { break; } - return this.decorateOrphanedFiles(title); + return this.decorateLabel(title); } - private decorateOrphanedFiles(label: string): string { + private decorateLabel(label: string): string { const model = this.textFileService.models.get(this.resource); if (model && model.hasState(ModelState.ORPHAN)) { return localize('orphanedFile', "{0} (deleted from disk)", label); } + if (model && model.isReadonly()) { + return localize('readonlyFile', "{0} (read-only)", label); + } return label; } - public isDirty(): boolean { + isDirty(): boolean { const model = this.textFileService.models.get(this.resource); if (!model) { return false; @@ -227,23 +224,23 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return model.isDirty(); } - public confirmSave(): TPromise { + confirmSave(): TPromise { return this.textFileService.confirmSave([this.resource]); } - public save(): TPromise { + save(): TPromise { return this.textFileService.save(this.resource); } - public revert(options?: IRevertOptions): TPromise { + revert(options?: IRevertOptions): TPromise { return this.textFileService.revert(this.resource, options); } - public getPreferredEditorId(candidates: string[]): string { + getPreferredEditorId(candidates: string[]): string { return this.forceOpenAsBinary ? BINARY_FILE_EDITOR_ID : TEXT_FILE_EDITOR_ID; } - public resolve(refresh?: boolean): TPromise { + resolve(): TPromise { // Resolve as binary if (this.forceOpenAsBinary) { @@ -251,13 +248,18 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } // Resolve as text - return this.doResolveAsText(refresh); + return this.doResolveAsText(); } - private doResolveAsText(reload?: boolean): TPromise { + private doResolveAsText(): TPromise { // Resolve as text - return this.textFileService.models.loadOrCreate(this.resource, { encoding: this.preferredEncoding, reload, allowBinary: this.forceOpenAsText }).then(model => { + return this.textFileService.models.loadOrCreate(this.resource, { + encoding: this.preferredEncoding, + reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model + allowBinary: this.forceOpenAsText, + reason: LoadReason.EDITOR + }).then(model => { // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into @@ -284,11 +286,11 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); } - public isResolved(): boolean { + isResolved(): boolean { return !!this.textFileService.models.get(this.resource); } - public getTelemetryDescriptor(): object { + getTelemetryDescriptor(): object { const descriptor = super.getTelemetryDescriptor(); descriptor['resource'] = telemetryURIDescriptor(this.getResource(), path => this.hashService.createSHA1(path)); @@ -300,21 +302,18 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return descriptor; } - public dispose(): void { + dispose(): void { // Model reference if (this.textModelReference) { - this.textModelReference.done(ref => ref.dispose()); + this.textModelReference.then(ref => ref.dispose()); this.textModelReference = null; } - // Listeners - this.toUnbind = dispose(this.toUnbind); - super.dispose(); } - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 1f01ef9d124..3425f0b9971 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -5,7 +5,7 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; @@ -14,9 +14,8 @@ import { IFileStat } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { toResource, IEditorIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { getPathLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; -import { startsWith, startsWithIgnoreCase, rtrim } from 'vs/base/common/strings'; +import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; export class Model { @@ -26,7 +25,7 @@ export class Model { constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, undefined, false, true, folder.name)); + .map(folder => new ExplorerItem(folder.uri, undefined, false, false, true, folder.name)); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => setRoots()); setRoots(); } @@ -73,18 +72,22 @@ export class ExplorerItem { public etag: string; private _isDirectory: boolean; private _isSymbolicLink: boolean; + private _isReadonly: boolean; private children: Map; + private _isError: boolean; public parent: ExplorerItem; public isDirectoryResolved: boolean; - constructor(resource: URI, public root: ExplorerItem, isSymbolicLink?: boolean, isDirectory?: boolean, name: string = getPathLabel(resource), mtime?: number, etag?: string) { + constructor(resource: URI, public root: ExplorerItem, isSymbolicLink?: boolean, isReadonly?: boolean, isDirectory?: boolean, name: string = resources.basenameOrAuthority(resource), mtime?: number, etag?: string, isError?: boolean) { this.resource = resource; this._name = name; this.isDirectory = !!isDirectory; this._isSymbolicLink = !!isSymbolicLink; + this._isReadonly = !!isReadonly; this.etag = etag; this.mtime = mtime; + this._isError = !!isError; if (!this.root) { this.root = this; @@ -101,6 +104,14 @@ export class ExplorerItem { return this._isDirectory; } + public get isReadonly(): boolean { + return this._isReadonly; + } + + public get isError(): boolean { + return this._isError; + } + public set isDirectory(value: boolean) { if (value !== this._isDirectory) { this._isDirectory = value; @@ -113,10 +124,6 @@ export class ExplorerItem { } - public get nonexistentRoot(): boolean { - return this.isRoot && !this.isDirectoryResolved && this.isDirectory; - } - public get name(): string { return this._name; } @@ -140,8 +147,8 @@ export class ExplorerItem { return this === this.root; } - public static create(raw: IFileStat, root: ExplorerItem, resolveTo?: URI[]): ExplorerItem { - const stat = new ExplorerItem(raw.resource, root, raw.isSymbolicLink, raw.isDirectory, raw.name, raw.mtime, raw.etag); + public static create(raw: IFileStat, root: ExplorerItem, resolveTo?: URI[], isError = false): ExplorerItem { + const stat = new ExplorerItem(raw.resource, root, raw.isSymbolicLink, raw.isReadonly, raw.isDirectory, raw.name, raw.mtime, raw.etag, isError); // Recursively add children if present if (stat.isDirectory) { @@ -150,7 +157,7 @@ export class ExplorerItem { // 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) => { - return resources.isEqualOrParent(r, stat.resource, !isLinux /* ignorecase */); + return resources.isEqualOrParent(r, stat.resource); })); // Recurse into children @@ -189,6 +196,8 @@ export class ExplorerItem { local.mtime = disk.mtime; local.isDirectoryResolved = disk.isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; + local._isReadonly = disk.isReadonly; + local._isError = disk.isError; // Merge Children if resolved if (mergingDirectories && disk.isDirectoryResolved) { @@ -270,19 +279,6 @@ export class ExplorerItem { return this.children.size; } - public getChildrenNames(): string[] { - if (!this.children) { - return []; - } - - const names: string[] = []; - this.children.forEach(child => { - names.push(child.name); - }); - - return names; - } - /** * Removes a child element from this folder. */ @@ -315,7 +311,7 @@ export class ExplorerItem { } private updateResource(recursive: boolean): void { - this.resource = this.parent.resource.with({ path: paths.join(this.parent.resource.path, this.name) }); + this.resource = resources.joinPath(this.parent.resource, this.name); if (recursive) { if (this.isDirectory && this.children) { @@ -346,9 +342,9 @@ export class ExplorerItem { */ public find(resource: URI): ExplorerItem { // Return if path found - if (resource && this.resource.scheme === resource.scheme && this.resource.authority === resource.authority && - (isLinux ? startsWith(resource.path, this.resource.path) : startsWithIgnoreCase(resource.path, this.resource.path)) - ) { + // For performance reasons try to do the comparison as fast as possible + if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && + (resources.hasToIgnoreCase(resource) ? startsWithIgnoreCase(resource.path, this.resource.path) : startsWith(resource.path, this.resource.path))) { return this.findByPath(rtrim(resource.path, paths.sep), this.resource.path.length); } @@ -396,7 +392,7 @@ export class NewStatPlaceholder extends ExplorerItem { private directoryPlaceholder: boolean; constructor(isDirectory: boolean, root: ExplorerItem) { - super(URI.file(''), root, false, false, NewStatPlaceholder.NAME); + super(URI.file(''), root, false, false, false, NewStatPlaceholder.NAME); this.id = NewStatPlaceholder.ID++; this.isDirectoryResolved = isDirectory; diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 26c9bd956ca..9db7cbedda6 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { IFilesConfiguration, FileChangeType, IFileService } from 'vs/platform/files/common/files'; @@ -13,7 +13,6 @@ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/con import { ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { ITextModel } from 'vs/editor/common/model'; import { IMode } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -50,10 +49,13 @@ const openEditorsVisibleId = 'openEditorsVisible'; const openEditorsFocusId = 'openEditorsFocus'; const explorerViewletFocusId = 'explorerViewletFocus'; const explorerResourceIsFolderId = 'explorerResourceIsFolder'; +const explorerResourceReadonly = 'explorerResourceReadonly'; const explorerResourceIsRootId = 'explorerResourceIsRoot'; 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 FilesExplorerFocusedContext = new RawContextKey(filesExplorerFocusId, true); export const OpenEditorsVisibleContext = new RawContextKey(openEditorsVisibleId, false); @@ -154,7 +156,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { ) { } - public provideTextContent(resource: URI): TPromise { + provideTextContent(resource: URI): TPromise { const fileOnDiskResource = URI.file(resource.fsPath); // Make sure our file from disk is resolved up to date @@ -164,7 +166,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { if (!this.fileWatcher) { this.fileWatcher = this.fileService.onFileChanges(changes => { if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) { - this.resolveEditorModel(resource, false /* do not create if missing */).done(null, onUnexpectedError); // update model when resource changes + this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } }); @@ -202,7 +204,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { }); } - public dispose(): void { + dispose(): void { this.fileWatcher = dispose(this.fileWatcher); } } diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index 57326f16477..257e5d35082 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -173,10 +173,10 @@ export class ExplorerViewlet extends ViewContainerViewlet implements IExplorerVi this._register(this.contextService.onDidChangeWorkspaceName(e => this.updateTitleArea())); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); - - DOM.addClass(parent, 'explorer-viewlet'); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + DOM.addClass(parent, 'explorer-viewlet'); + }); } protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel { 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 bdc586291c7..3db81f99473 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -6,25 +6,26 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, CopyPathAction, FocusOpenEditorsView, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, FocusOpenEditorsView, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/parts/files/electron-browser/saveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, REVEAL_IN_OS_COMMAND_ID, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, REVEAL_IN_OS_LABEL, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; +import { openWindowCommand, REVEAL_IN_OS_COMMAND_ID, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, REVEAL_IN_OS_LABEL, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext } from 'vs/workbench/parts/files/common/files'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext } 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'; import { AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; +import { FileDialogContext } from 'vs/platform/workbench/common/contextkeys'; // Contribute Global Actions const category = nls.localize('filesCategory', "File"); @@ -40,6 +41,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshExplorerView, R registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'File: New Untitled File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category); registry.registerWorkbenchAction(new SyncActionDescriptor(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category); // Commands CommandsRegistry.registerCommand('_files.windowOpen', openWindowCommand); @@ -49,8 +51,8 @@ const explorerCommandsWeightBonus = 10; // give our commands a little bit more w const RENAME_ID = 'renameFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: RENAME_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyCode.F2, mac: { primary: KeyCode.Enter @@ -61,8 +63,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_FILE_TO_TRASH_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -73,8 +75,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const DELETE_FILE_ID = 'deleteFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyMod.Shift | KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace @@ -85,7 +87,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const COPY_FILE_ID = 'filesExplorer.copy'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: COPY_FILE_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()), primary: KeyMod.CtrlCmd | KeyCode.KEY_C, handler: copyFileHandler, @@ -95,45 +97,56 @@ const PASTE_FILE_ID = 'filesExplorer.paste'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: PASTE_FILE_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(explorerCommandsWeightBonus), - when: ContextKeyExpr.and(FilesExplorerFocusCondition), + weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), primary: KeyMod.CtrlCmd | KeyCode.KEY_V, handler: pasteFileHandler }); +const copyPathCommand = { + id: COPY_PATH_COMMAND_ID, + title: nls.localize('copyPath', "Copy Path") +}; + +const copyRelativePathCommand = { + id: COPY_RELATIVE_PATH_COMMAND_ID, + title: nls.localize('copyRelativePath', "Copy Relative Path") +}; + // Editor Title Context Menu appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, ResourceContextKey.Scheme.isEqualTo(Schemas.file)); -appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, CopyPathAction.LABEL, ResourceContextKey.IsFile); -appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Side Bar"), ResourceContextKey.IsFile); +appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, copyRelativePathCommand); +appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Side Bar"), ResourceContextKey.IsFileSystemResource); -function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr): void { +function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr, alt?: { id: string, title: string }): void { // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id, title }, when, - group: '2_files' + group: '2_files', + alt }); } // Editor Title Menu for Conflict Resolution appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), { - light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check.svg`)).fsPath, - dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check-inverse.svg`)).fsPath + light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check.svg`)), + dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/check-inverse.svg`)) }, -10, acceptLocalChangesCommand); appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), { - light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo.svg`)).fsPath, - dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo-inverse.svg`)).fsPath + light: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo.svg`)), + dark: URI.parse(require.toUrl(`vs/workbench/parts/files/electron-browser/media/undo-inverse.svg`)) }, -9, revertLocalChangesCommand); -function appendSaveConflictEditorTitleAction(id: string, title: string, iconPath: { dark: string; light?: string; }, order: number, command: ICommandHandler): void { +function appendSaveConflictEditorTitleAction(id: string, title: string, iconLocation: { dark: URI; light?: URI; }, order: number, command: ICommandHandler): void { // Command CommandsRegistry.registerCommand(id, command); // Action MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { id, title, iconPath }, + command: { id, title, iconLocation }, when: ContextKeyExpr.equals(CONFLICT_RESOLUTION_CONTEXT, true), group: 'navigation', order @@ -142,29 +155,29 @@ function appendSaveConflictEditorTitleAction(id: string, title: string, iconPath // Menu registration - command palette -function appendToCommandPalette(id: string, title: string, category: string): void { +function appendToCommandPalette(id: string, title: string, category: string, when?: ContextKeyExpr): void { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id, title, category - } + }, + when }); } appendToCommandPalette(COPY_PATH_COMMAND_ID, nls.localize('copyPathOfActive', "Copy Path of Active File"), category); +appendToCommandPalette(COPY_RELATIVE_PATH_COMMAND_ID, nls.localize('copyRelativePathOfActive', "Copy Relative Path of Active File"), category); appendToCommandPalette(SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, category); appendToCommandPalette(SAVE_ALL_IN_GROUP_COMMAND_ID, nls.localize('saveAllInGroup', "Save All in Group"), category); appendToCommandPalette(SAVE_FILES_COMMAND_ID, nls.localize('saveFiles', "Save All Files"), category); appendToCommandPalette(REVERT_FILE_COMMAND_ID, nls.localize('revert', "Revert File"), category); appendToCommandPalette(COMPARE_WITH_SAVED_COMMAND_ID, nls.localize('compareActiveWithSaved', "Compare Active File with Saved"), category); appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, category); -appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, category); +appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, category, FileDialogContext.isEqualTo('local')); appendToCommandPalette(CLOSE_EDITOR_COMMAND_ID, nls.localize('closeEditor', "Close Editor"), nls.localize('view', "View")); appendToCommandPalette(NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, category); appendToCommandPalette(NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, category); - - // Menu registration - open editors const openToSideCommand = { @@ -175,7 +188,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 10, command: openToSideCommand, - when: ResourceContextKey.IsFile + when: ResourceContextKey.IsFileSystemResource }); const revealInOsCommand = { @@ -186,18 +199,15 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 20, command: revealInOsCommand, - when: ResourceContextKey.Scheme.isEqualTo(Schemas.file) + when: ResourceContextKey.IsFileSystemResource }); -const copyPathCommand = { - id: COPY_PATH_COMMAND_ID, - title: nls.localize('copyPath', "Copy Path") -}; MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 40, command: copyPathCommand, - when: ResourceContextKey.IsFile + alt: copyRelativePathCommand, + when: ResourceContextKey.IsFileSystemResource }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -208,7 +218,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: SAVE_FILE_LABEL, precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFile, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -219,7 +229,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('revert', "Revert File"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFile, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -248,7 +258,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('compareWithSaved', "Compare with Saved"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFile, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''), WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''), WorkbenchListDoubleSelection.toNegated()) }); const compareResourceCommand = { @@ -259,7 +269,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 20, command: compareResourceCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResourceOrUntitled, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated()) }); const selectForCompareCommand = { @@ -270,7 +280,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 30, command: selectForCompareCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResourceOrUntitled, WorkbenchListDoubleSelection.toNegated()) }); const compareSelectedCommand = { @@ -281,7 +291,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResourceOrUntitled, WorkbenchListDoubleSelection) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -329,7 +339,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 4, command: { id: NEW_FILE_COMMAND_ID, - title: NEW_FILE_LABEL + title: NEW_FILE_LABEL, + precondition: ExplorerResourceNotReadonlyContext }, when: ExplorerFolderContext }); @@ -339,7 +350,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 6, command: { id: NEW_FOLDER_COMMAND_ID, - title: NEW_FOLDER_LABEL + title: NEW_FOLDER_LABEL, + precondition: ExplorerResourceNotReadonlyContext }, when: ExplorerFolderContext }); @@ -362,21 +374,21 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '3_compare', order: 20, command: compareResourceCommand, - when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.IsFile, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated()) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '3_compare', order: 30, command: selectForCompareCommand, - when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.IsFile, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, WorkbenchListDoubleSelection.toNegated()) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.IsFile, WorkbenchListDoubleSelection) + when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, WorkbenchListDoubleSelection) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -404,7 +416,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '5_cutcopypaste', order: 30, command: copyPathCommand, - when: ResourceContextKey.IsFile + alt: copyRelativePathCommand, + when: ResourceContextKey.IsFileSystemResource }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -414,7 +427,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: ADD_ROOT_FOLDER_COMMAND_ID, title: ADD_ROOT_FOLDER_LABEL }, - when: ExplorerRootContext + when: ContextKeyExpr.and(ExplorerRootContext, FileDialogContext.isEqualTo('local')) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -442,7 +455,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 10, command: { id: RENAME_ID, - title: TRIGGER_RENAME_LABEL + title: TRIGGER_RENAME_LABEL, + precondition: ExplorerResourceNotReadonlyContext }, when: ExplorerRootContext.toNegated() }); @@ -452,15 +466,95 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: MOVE_FILE_TO_TRASH_ID, - title: MOVE_FILE_TO_TRASH_LABEL + title: MOVE_FILE_TO_TRASH_LABEL, + precondition: ExplorerResourceNotReadonlyContext }, alt: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently") + title: nls.localize('deleteFile', "Delete Permanently"), + precondition: ExplorerResourceNotReadonlyContext }, when: ExplorerRootContext.toNegated() }); // Empty Editor Group Context Menu -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.files.newUntitledFile', title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); \ No newline at end of file +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: GlobalNewUntitledFileAction.ID, title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); + +// File menu + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: GlobalNewUntitledFileAction.ID, + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SAVE_FILE_COMMAND_ID, + title: nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SAVE_FILE_AS_COMMAND_ID, + title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As...") + }, + order: 2, + when: FileDialogContext.isEqualTo('local') +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: SaveAllAction.ID, + title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") + }, + order: 3 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '5_autosave', + command: { + id: ToggleAutoSaveAction.ID, + title: nls.localize('miAutoSave', "Auto Save") + }, + order: 1 +}); + +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 + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: CLOSE_EDITOR_COMMAND_ID, + title: nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor") + }, + order: 2 +}); + +// Go to menu + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.quickOpen', + title: nls.localize({ key: 'miGotoFile', comment: ['&& denotes a mnemonic'] }, "Go to &&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 725875e9e81..46d4ba300f2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -8,23 +8,21 @@ import 'vs/css!./media/fileactions'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import * as types from 'vs/base/common/types'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { sequence, ITask, 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'; -import { posix } from 'path'; -import * as errors from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; -import * as diagnostics from 'vs/base/common/diagnostics'; import { Action, IAction } from 'vs/base/common/actions'; import { MessageType, IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITree, IHighlightEvent } from 'vs/base/parts/tree/browser/tree'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; @@ -35,9 +33,8 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService, ServicesAccessor, IConstructorSignature2 } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWindowService } from 'vs/platform/windows/common/windows'; -import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/parts/files/electron-browser/fileCommands'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -209,11 +206,11 @@ class TriggerRenameFileAction extends BaseFileAction { const unbind = this.tree.onDidChangeHighlight((e: IHighlightEvent) => { if (!e.highlight) { viewletState.clearEditable(stat); - this.tree.refresh(stat).done(null, errors.onUnexpectedError); + this.tree.refresh(stat); unbind.dispose(); } }); - }).done(null, errors.onUnexpectedError); + }); return void 0; } @@ -234,6 +231,10 @@ export abstract class BaseRenameAction extends BaseFileAction { this.element = element; } + _isEnabled(): boolean { + return super._isEnabled() && this.element && !this.element.isReadonly; + } + public run(context?: any): TPromise { if (!context) { return TPromise.wrapError(new Error('No context provided to BaseRenameFileAction.')); @@ -288,8 +289,7 @@ class RenameFileAction extends BaseRenameAction { element: ExplorerItem, @IFileService fileService: IFileService, @INotificationService notificationService: INotificationService, - @ITextFileService textFileService: ITextFileService, - @IBackupFileService private backupFileService: IBackupFileService + @ITextFileService textFileService: ITextFileService ) { super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, fileService, notificationService, textFileService); @@ -297,43 +297,10 @@ class RenameFileAction extends BaseRenameAction { } public runAction(newName: string): TPromise { - const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, this.element.resource, !isLinux /* ignorecase */)); - const dirtyRenamed: URI[] = []; - return TPromise.join(dirty.map(d => { - let renamed: URI; + const parentResource = this.element.parent.resource; + const targetResource = resources.joinPath(parentResource, newName); - // If the dirty file itself got moved, just reparent it to the target folder - const targetPath = paths.join(this.element.parent.resource.path, newName); - if (this.element.resource.toString() === d.toString()) { - renamed = this.element.parent.resource.with({ path: targetPath }); - } - - // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: - else { - renamed = this.element.parent.resource.with({ path: paths.join(targetPath, d.path.substr(this.element.resource.path.length + 1)) }); - } - - dirtyRenamed.push(renamed); - - const model = this.textFileService.models.get(d); - - return this.backupFileService.backupResource(renamed, model.createSnapshot(), model.getVersionId()); - })) - - // 2. soft revert all dirty since we have backed up their contents - .then(() => this.textFileService.revertAll(dirty, { soft: true /* do not attempt to load content from disk */ })) - - // 3.) run the rename operation - .then(() => this.fileService.rename(this.element.resource, newName).then(null, (error: Error) => { - return TPromise.join(dirtyRenamed.map(d => this.backupFileService.discardResourceBackup(d))).then(() => { - this.onErrorWithRetry(error, () => this.runAction(newName)); - }); - })) - - // 4.) resolve those that were dirty to load their previous dirty contents from disk - .then(() => { - return TPromise.join(dirtyRenamed.map(t => this.textFileService.models.loadOrCreate(t))); - }); + return this.textFileService.move(this.element.resource, targetResource); } } @@ -390,6 +357,9 @@ export class BaseNewAction extends BaseFileAction { if (!folder) { return TPromise.wrapError(new Error('Invalid parent folder to create.')); } + if (folder.isReadonly) { + return TPromise.wrapError(new Error('Parent folder is readonly.')); + } if (!!folder.getChild(NewStatPlaceholder.NAME)) { // Do not allow to creatae a new file/folder while in the process of creating a new file/folder #47606 return TPromise.as(new Error('Parent folder is already in the process of creating a file')); @@ -426,7 +396,7 @@ export class BaseNewAction extends BaseFileAction { const unbind = this.tree.onDidChangeHighlight((e: IHighlightEvent) => { if (!e.highlight) { stat.destroy(); - this.tree.refresh(folder).done(null, errors.onUnexpectedError); + this.tree.refresh(folder); unbind.dispose(); } }); @@ -524,7 +494,7 @@ class CreateFileAction extends BaseCreateAction { public runAction(fileName: string): TPromise { const resource = this.element.parent.resource; - return this.fileService.createFile(resource.with({ path: paths.join(resource.path, fileName) })).then(stat => { + return this.fileService.createFile(resources.joinPath(resource, fileName)).then(stat => { return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); }, (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); @@ -551,7 +521,7 @@ class CreateFolderAction extends BaseCreateAction { public runAction(fileName: string): TPromise { const resource = this.element.parent.resource; - return this.fileService.createFolder(resource.with({ path: paths.join(resource.path, fileName) })).then(null, (error) => { + return this.fileService.createFolder(resources.joinPath(resource, fileName)).then(null, (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); }); } @@ -581,6 +551,10 @@ class BaseDeleteFileAction extends BaseFileAction { this._updateEnablement(); } + _isEnabled(): boolean { + return super._isEnabled() && this.elements && this.elements.every(e => !e.isReadonly); + } + public run(): TPromise { // Remove highlight @@ -684,7 +658,7 @@ class BaseDeleteFileAction extends BaseFileAction { } // Call function - const servicePromise = TPromise.join(distinctElements.map(e => this.fileService.del(e.resource, this.useTrash))).then(() => { + const servicePromise = TPromise.join(distinctElements.map(e => this.fileService.del(e.resource, { useTrash: this.useTrash, recursive: true }))).then(() => { if (distinctElements[0].parent) { this.tree.setFocus(distinctElements[0].parent); // move focus to parent } @@ -808,9 +782,9 @@ export class AddFilesAction extends BaseFileAction { this._updateEnablement(); } - public run(resources: URI[]): TPromise { + public run(resourcesToAdd: URI[]): TPromise { const addPromise = TPromise.as(null).then(() => { - if (resources && resources.length > 0) { + if (resourcesToAdd && resourcesToAdd.length > 0) { // Find parent to add to let targetElement: ExplorerItem; @@ -835,8 +809,8 @@ export class AddFilesAction extends BaseFileAction { }); let overwritePromise: TPromise = TPromise.as({ confirmed: true }); - if (resources.some(resource => { - return targetNames.has(isLinux ? paths.basename(resource.fsPath) : paths.basename(resource.fsPath).toLowerCase()); + if (resourcesToAdd.some(resource => { + return targetNames.has(!resources.hasToIgnoreCase(resource) ? resources.basename(resource) : resources.basename(resource).toLowerCase()); })) { const confirm: IConfirmation = { message: nls.localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), @@ -855,10 +829,10 @@ export class AddFilesAction extends BaseFileAction { // Run add in sequence const addPromisesFactory: ITask>[] = []; - resources.forEach(resource => { + resourcesToAdd.forEach(resource => { addPromisesFactory.push(() => { const sourceFile = resource; - const targetFile = targetElement.resource.with({ path: paths.join(targetElement.resource.path, paths.basename(sourceFile.path)) }); + const targetFile = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents // of the target file would replace the contents of the added file. since we already @@ -869,11 +843,11 @@ export class AddFilesAction extends BaseFileAction { } return revertPromise.then(() => { - const target = targetElement.resource.with({ path: posix.join(targetElement.resource.path, posix.basename(sourceFile.path)) }); + const target = resources.joinPath(targetElement.resource, resources.basename(sourceFile)); return this.fileService.copyFile(sourceFile, target, true).then(stat => { // if we only add one file, just open it directly - if (resources.length === 1) { + if (resourcesToAdd.length === 1) { this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); } }, error => this.onError(error)); @@ -1044,14 +1018,14 @@ export class DuplicateFileAction extends BaseFileAction { function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean }): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); - let candidate = targetFolder.resource.with({ path: paths.join(targetFolder.resource.path, name) }); + let candidate = resources.joinPath(targetFolder.resource, name); while (true) { if (!targetFolder.root.find(candidate)) { break; } name = incrementFileName(name, fileToPaste.isDirectory); - candidate = targetFolder.resource.with({ path: paths.join(targetFolder.resource.path, name) }); + candidate = resources.joinPath(targetFolder.resource, name); } return candidate; @@ -1185,6 +1159,36 @@ export class RefreshViewExplorerAction extends Action { } } +export class ToggleAutoSaveAction extends Action { + public static readonly ID = 'workbench.action.toggleAutoSave'; + public static readonly LABEL = nls.localize('toggleAutoSave', "Toggle Auto Save"); + + constructor( + id: string, + label: string, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(id, label); + } + + public run(): TPromise { + const setting = this.configurationService.inspect('files.autoSave'); + let userAutoSaveConfig = setting.user; + if (types.isUndefinedOrNull(userAutoSaveConfig)) { + userAutoSaveConfig = setting.default; // use default if setting not defined + } + + let newAutoSaveValue: string; + if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) { + newAutoSaveValue = AutoSaveConfiguration.OFF; + } else { + newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY; + } + + return this.configurationService.updateValue('files.autoSave', newAutoSaveValue, ConfigurationTarget.USER); + } +} + export abstract class BaseSaveAllAction extends BaseErrorReportingAction { private toDispose: IDisposable[]; private lastIsDirty: boolean; @@ -1388,7 +1392,7 @@ export class CollapseExplorerView extends Action { const viewer = explorerView.getViewer(); if (viewer) { const action = new CollapseAction(viewer, true, null); - action.run().done(); + action.run(); action.dispose(); } } @@ -1437,7 +1441,7 @@ export class ShowOpenedFileInNewWindow extends Action { public run(): TPromise { const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: true, filter: Schemas.file /* todo@remote */ }); if (fileResource) { - this.windowService.openWindow([fileResource.fsPath], { forceNewWindow: true, forceOpenWorkspaceAsFile: true }); + this.windowService.openWindow([fileResource], { forceNewWindow: true, forceOpenWorkspaceAsFile: true }); } else { this.notificationService.info(nls.localize('openFileToShowInNewWindow', "Open a file first to open in new window")); } @@ -1446,24 +1450,6 @@ export class ShowOpenedFileInNewWindow extends Action { } } -export class CopyPathAction extends Action { - - public static readonly LABEL = nls.localize('copyPath', "Copy Path"); - - constructor( - private resource: URI, - @ICommandService private commandService: ICommandService - ) { - super('copyFilePath', CopyPathAction.LABEL); - - this.order = 140; - } - - public run(): TPromise { - return this.commandService.executeCommand(COPY_PATH_COMMAND_ID, this.resource); - } -} - export function validateFileName(parent: ExplorerItem, name: string): string { // Produce a well formed file name @@ -1557,14 +1543,14 @@ export class CompareWithClipboardAction extends Action { this.registrationDisposal = this.textModelService.registerTextModelContentProvider(CompareWithClipboardAction.SCHEME, provider); } - const name = paths.basename(resource.fsPath); + const name = resources.basename(resource); const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name); const cleanUp = () => { this.registrationDisposal = dispose(this.registrationDisposal); }; - return always(this.editorService.openEditor({ leftResource: URI.from({ scheme: CompareWithClipboardAction.SCHEME, path: resource.fsPath }), rightResource: resource, label: editorLabel }), cleanUp); + return always(this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }), cleanUp); } return TPromise.as(true); @@ -1591,14 +1577,6 @@ class ClipboardContentProvider implements ITextModelContentProvider { } } -// Diagnostics support -let diag: (...args: any[]) => void; -if (!diag) { - diag = diagnostics.register('FileActionsDiagnostics', function (...args: any[]) { - console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])'); - }); -} - interface IExplorerContext { viewletState: IFileViewletState; stat: ExplorerItem; diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index 199d769c59d..31078d8e8eb 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -8,8 +8,7 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as labels from 'vs/base/common/labels'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { toResource, IEditorCommandsContext } from 'vs/workbench/common/editor'; import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -29,7 +28,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -42,6 +41,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ILabelService } from 'vs/platform/label/common/label'; // Commands @@ -56,6 +56,7 @@ export const COMPARE_SELECTED_COMMAND_ID = 'compareSelected'; export const COMPARE_RESOURCE_COMMAND_ID = 'compareFiles'; export const COMPARE_WITH_SAVED_COMMAND_ID = 'workbench.files.action.compareWithSaved'; export const COPY_PATH_COMMAND_ID = 'copyFilePath'; +export const COPY_RELATIVE_PATH_COMMAND_ID = 'copyRelativeFilePath'; export const SAVE_FILE_AS_COMMAND_ID = 'workbench.action.files.saveAs'; export const SAVE_FILE_AS_LABEL = nls.localize('saveAs', "Save As..."); @@ -76,14 +77,20 @@ export const ResourceSelectedForCompareContext = new RawContextKey('res export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); -export const openWindowCommand = (accessor: ServicesAccessor, paths: string[], forceNewWindow: boolean) => { +export const openWindowCommand = (accessor: ServicesAccessor, paths: (string | URI)[], forceNewWindow: boolean) => { const windowService = accessor.get(IWindowService); - - windowService.openWindow(paths, { forceNewWindow }); + windowService.openWindow(paths.map(p => typeof p === 'string' ? URI.file(p) : p), { forceNewWindow }); }; -function save(resource: URI, isSaveAs: boolean, editorService: IEditorService, fileService: IFileService, untitledEditorService: IUntitledEditorService, - textFileService: ITextFileService, editorGroupService: IEditorGroupsService): TPromise { +function save( + resource: URI, + isSaveAs: boolean, + editorService: IEditorService, + fileService: IFileService, + untitledEditorService: IUntitledEditorService, + textFileService: ITextFileService, + editorGroupService: IEditorGroupsService +): TPromise { if (resource && (fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) { @@ -111,7 +118,7 @@ function save(resource: URI, isSaveAs: boolean, editorService: IEditorService, f if (!isSaveAs && resource.scheme === Schemas.untitled && untitledEditorService.hasAssociatedFilePath(resource)) { savePromise = textFileService.save(resource).then((result) => { if (result) { - return URI.file(resource.fsPath); + return resource.with({ scheme: Schemas.file }); } return null; @@ -166,9 +173,10 @@ function saveAll(saveAllArguments: any, editorService: IEditorService, untitledE const groupIdToUntitledResourceInput = new Map(); editorGroupService.groups.forEach(g => { + const activeEditorResource = g.activeEditor && g.activeEditor.getResource(); g.editors.forEach(e => { const resource = e.getResource(); - if (untitledEditorService.isDirty(resource)) { + if (resource && untitledEditorService.isDirty(resource)) { if (!groupIdToUntitledResourceInput.has(g.id)) { groupIdToUntitledResourceInput.set(g.id, []); } @@ -177,7 +185,7 @@ function saveAll(saveAllArguments: any, editorService: IEditorService, untitledE encoding: untitledEditorService.getEncoding(resource), resource, options: { - inactive: g.activeEditor ? g.activeEditor.getResource().toString() !== resource.toString() : true, + inactive: activeEditorResource ? activeEditorResource.toString() !== resource.toString() : true, pinned: true, preserveFocus: true, index: g.getIndexOfEditor(e) @@ -224,7 +232,7 @@ CommandsRegistry.registerCommand({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ExplorerFocusCondition, primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { @@ -262,7 +270,7 @@ let provider: FileOnDiskContentProvider; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: COMPARE_WITH_SAVED_COMMAND_ID, when: undefined, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_D), handler: (accessor, resource: URI | object) => { if (!provider) { @@ -279,7 +287,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const name = paths.basename(uri.fsPath); const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name); - return editorService.openEditor({ leftResource: URI.from({ scheme: COMPARE_WITH_SAVED_SCHEMA, path: uri.fsPath }), rightResource: uri, label: editorLabel }).then(() => void 0); + return editorService.openEditor({ leftResource: uri.with({ scheme: COMPARE_WITH_SAVED_SCHEMA }), rightResource: uri, label: editorLabel }).then(() => void 0); } return TPromise.as(true); @@ -352,9 +360,10 @@ function revealResourcesInOS(resources: URI[], windowsService: IWindowsService, notificationService.info(nls.localize('openFileToReveal', "Open a file first to reveal")); } } + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REVEAL_IN_OS_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.focus.toNegated(), primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R, win: { @@ -365,8 +374,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_R), id: 'workbench.action.files.revealActiveFileInWindows', @@ -378,17 +388,20 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -function resourcesToClipboard(resources: URI[], clipboardService: IClipboardService, notificationService: INotificationService): void { +function resourcesToClipboard(resources: URI[], relative: boolean, clipboardService: IClipboardService, notificationService: INotificationService, labelService: ILabelService): void { if (resources.length) { const lineDelimiter = isWindows ? '\r\n' : '\n'; - const text = resources.map(r => r.scheme === Schemas.file ? labels.getPathLabel(r) : r.toString()).join(lineDelimiter); + + const text = resources.map(resource => labelService.getUriLabel(resource, relative, true)) + .join(lineDelimiter); clipboardService.writeText(text); } else { notificationService.info(nls.localize('openFileToCopy', "Open a file first to copy its path")); } } + KeybindingsRegistry.registerCommandAndKeybindingRule({ - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.focus.toNegated(), primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C, win: { @@ -397,12 +410,26 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: COPY_PATH_COMMAND_ID, handler: (accessor, resource: URI | object) => { const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); - resourcesToClipboard(resources, accessor.get(IClipboardService), accessor.get(INotificationService)); + resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, + when: EditorContextKeys.focus.toNegated(), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C, + win: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C) + }, + id: COPY_RELATIVE_PATH_COMMAND_ID, + handler: (accessor, resource: URI | object) => { + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); + resourcesToClipboard(resources, true, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_P), id: 'workbench.action.files.copyPathOfActiveFile', @@ -410,7 +437,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const editorService = accessor.get(IEditorService); const activeInput = editorService.activeEditor; const resources = activeInput && activeInput.getResource() ? [activeInput.getResource()] : []; - resourcesToClipboard(resources, accessor.get(IClipboardService), accessor.get(INotificationService)); + resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); } }); @@ -441,7 +468,7 @@ CommandsRegistry.registerCommand({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SAVE_FILE_AS_COMMAND_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, handler: (accessor, resourceOrObject: URI | object | { from: string }) => { @@ -459,7 +486,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_S, id: SAVE_FILE_COMMAND_ID, handler: (accessor, resource: URI | object) => { diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index 71c741536a2..10ba8288bb7 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -5,14 +5,14 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ToggleViewletAction } from 'vs/workbench/browser/viewlet'; import * as nls from 'vs/nls'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, SUPPORTED_ENCODINGS } from 'vs/platform/files/common/files'; import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID } from 'vs/workbench/parts/files/common/files'; @@ -34,6 +34,9 @@ import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInpu import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { Schemas } from 'vs/base/common/network'; +import { nativeSep } from 'vs/base/common/paths'; // Viewlet Action export class OpenExplorerViewletAction extends ToggleViewletAction { @@ -50,6 +53,23 @@ export class OpenExplorerViewletAction extends ToggleViewletAction { } } +class FileUriLabelContribution implements IWorkbenchContribution { + + constructor(@ILabelService labelService: ILabelService) { + labelService.registerFormatter(Schemas.file, { + uri: { + label: '${path}', + separator: nativeSep, + tildify: !platform.isWindows, + normalizeDriveLetter: platform.isWindows + }, + workspace: { + suffix: '' + } + }); + } +} + // Register Viewlet Registry.as(ViewletExtensions.Viewlets).registerViewlet(new ViewletDescriptor( ExplorerViewlet, @@ -137,7 +157,7 @@ class FileEditorInputFactory implements IEditorInputFactory { const resource = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource); const encoding = fileInput.encoding; - return accessor.get(IEditorService).createInput({ resource, encoding }) as FileEditorInput; + return accessor.get(IEditorService).createInput({ resource, encoding, forceFile: true }) as FileEditorInput; }); } } @@ -156,6 +176,10 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Register Dirty Files Tracker Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesTracker, LifecyclePhase.Starting); +// Register uri display for file uris +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting); + + // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -167,7 +191,7 @@ configurationRegistry.registerConfiguration({ 'properties': { 'files.exclude': { 'type': 'object', - 'description': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting."), + 'markdownDescription': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), 'default': { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true }, 'scope': ConfigurationScope.RESOURCE, 'additionalProperties': { @@ -192,14 +216,14 @@ configurationRegistry.registerConfiguration({ }, 'files.associations': { 'type': 'object', - 'description': nls.localize('associations', "Configure file associations to languages (e.g. \"*.extension\": \"html\"). These have precedence over the default associations of the languages installed."), + 'markdownDescription': nls.localize('associations', "Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."), }, 'files.encoding': { 'type': 'string', 'overridable': true, 'enum': Object.keys(SUPPORTED_ENCODINGS), 'default': 'utf8', - 'description': nls.localize('encoding', "The default character set encoding to use when reading and writing files. This setting can be configured per language too."), + 'description': nls.localize('encoding', "The default character set encoding to use when reading and writing files. This setting can also be configured per language."), 'scope': ConfigurationScope.RESOURCE, 'enumDescriptions': Object.keys(SUPPORTED_ENCODINGS).map(key => SUPPORTED_ENCODINGS[key].labelLong) }, @@ -207,7 +231,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'overridable': true, 'default': false, - 'description': nls.localize('autoGuessEncoding', "When enabled, will attempt to guess the character set encoding when opening files. This setting can be configured per language too."), + 'description': nls.localize('autoGuessEncoding', "When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language."), 'scope': ConfigurationScope.RESOURCE }, 'files.eol': { @@ -216,8 +240,12 @@ configurationRegistry.registerConfiguration({ '\n', '\r\n' ], + 'enumDescriptions': [ + nls.localize('eol.LF', "LF"), + nls.localize('eol.CRLF', "CRLF") + ], 'default': (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n', - 'description': nls.localize('eol', "The default end of line character. Use \\n for LF and \\r\\n for CRLF."), + 'description': nls.localize('eol', "The default end of line character."), 'scope': ConfigurationScope.RESOURCE }, 'files.trimTrailingWhitespace': { @@ -244,19 +272,19 @@ configurationRegistry.registerConfiguration({ 'files.autoSave': { 'type': 'string', 'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.off' }, "A dirty file is never automatically saved."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty file is automatically saved after the configured 'files.autoSaveDelay'."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty file is automatically saved after the configured `#files.autoSaveDelay#`."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty file is automatically saved when the editor loses focus."), nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty file is automatically saved when the window loses focus.") ], 'default': AutoSaveConfiguration.OFF, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Accepted values: '{0}', '{1}', '{2}' (editor loses focus), '{3}' (window loses focus). If set to '{4}', you can configure the delay in 'files.autoSaveDelay'.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) }, 'files.autoSaveDelay': { 'type': 'number', 'default': 1000, - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when 'files.autoSave' is set to '{0}'", AutoSaveConfiguration.AFTER_DELAY) + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) }, 'files.watcherExclude': { 'type': 'object', @@ -268,10 +296,10 @@ configurationRegistry.registerConfiguration({ 'type': 'string', 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT, - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localize('hotExit.off', 'Disable hot exit.'), - nls.localize('hotExit.onExit', 'Hot exit will be triggered when the application is closed, that is when the last window is closed on Windows/Linux or when the workbench.action.quit command is triggered (command palette, keybinding, menu). All windows with backups will be restored upon next launch.'), - nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the application is closed, that is when the last window is closed on Windows/Linux or when the workbench.action.quit command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. To restore folder windows as they were before shutdown set "window.restoreWindows" to "all".') + nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows with backups will be restored upon next launch.'), + nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. To restore folder windows as they were before shutdown set `#window.restoreWindows#` to `all`.') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) }, @@ -287,7 +315,7 @@ configurationRegistry.registerConfiguration({ 'files.maxMemoryForLargeFilesMB': { 'type': 'number', 'default': 4096, - 'description': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying --max-memory=NEWSIZE on the command line.") + 'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line.") } } }); @@ -308,7 +336,7 @@ configurationRegistry.registerConfiguration({ 'editor.formatOnSaveTimeout': { 'type': 'number', 'default': 750, - 'description': nls.localize('formatOnSaveTimeout', "Format on save timeout. Specifies a time limit in milliseconds for formatOnSave-commands. Commands taking longer than the specified timeout will be cancelled."), + 'description': nls.localize('formatOnSaveTimeout', "Timeout in milliseconds after which the formatting that is run on file save is cancelled."), 'overridable': true, 'scope': ConfigurationScope.RESOURCE } @@ -328,22 +356,22 @@ configurationRegistry.registerConfiguration({ }, 'explorer.autoReveal': { 'type': 'boolean', - 'description': nls.localize('autoReveal', "Controls if the explorer should automatically reveal and select files when opening them."), + 'description': nls.localize('autoReveal', "Controls whether the explorer should automatically reveal and select files when opening them."), 'default': true }, 'explorer.enableDragAndDrop': { 'type': 'boolean', - 'description': nls.localize('enableDragAndDrop', "Controls if the explorer should allow to move files and folders via drag and drop."), + 'description': nls.localize('enableDragAndDrop', "Controls whether the explorer should allow to move files and folders via drag and drop."), 'default': true }, 'explorer.confirmDragAndDrop': { 'type': 'boolean', - 'description': nls.localize('confirmDragAndDrop', "Controls if the explorer should ask for confirmation to move files and folders via drag and drop."), + 'description': nls.localize('confirmDragAndDrop', "Controls whether the explorer should ask for confirmation to move files and folders via drag and drop."), 'default': true }, 'explorer.confirmDelete': { 'type': 'boolean', - 'description': nls.localize('confirmDelete', "Controls if the explorer should ask for confirmation when deleting a file via the trash."), + 'description': nls.localize('confirmDelete', "Controls whether the explorer should ask for confirmation when deleting a file via the trash."), 'default': true }, 'explorer.sortOrder': { @@ -357,17 +385,27 @@ configurationRegistry.registerConfiguration({ nls.localize('sortOrder.type', 'Files and folders are sorted by their extensions, in alphabetical order. Folders are displayed before files.'), nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date, in descending order. Folders are displayed before files.') ], - 'description': nls.localize({ key: 'sortOrder', comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'] }, "Controls sorting order of files and folders in the explorer. In addition to the default sorting, you can set the order to 'mixed' (files and folders sorted combined), 'type' (by file type), 'modified' (by last modified date) or 'filesFirst' (sort files before folders).") + 'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.") }, 'explorer.decorations.colors': { type: 'boolean', - description: nls.localize('explorer.decorations.colors', "Controls if file decorations should use colors."), + description: nls.localize('explorer.decorations.colors', "Controls whether file decorations should use colors."), default: true }, 'explorer.decorations.badges': { type: 'boolean', - description: nls.localize('explorer.decorations.badges', "Controls if file decorations should use badges."), + description: nls.localize('explorer.decorations.badges', "Controls whether file decorations should use badges."), default: true }, } }); + +// View menu +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") + }, + order: 1 +}); 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 eb657380a67..c3fc2504f70 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -59,7 +59,7 @@ } .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-list.focused .monaco-list-row.focused > .monaco-action-bar, +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.focused > .monaco-action-bar, .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar { visibility: visible; } diff --git a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index ca120f667fc..27a3136bda3 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -6,15 +6,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as paths from 'vs/base/common/paths'; import { Action } from 'vs/base/common/actions'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -43,9 +42,8 @@ const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError'; const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content on disk with your changes."); // A handler for save error happening with conflict resolution actions -export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContribution { +export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { private messages: ResourceMap; - private toUnbind: IDisposable[]; private conflictResolutionContext: IContextKey; private activeConflictResolutionResource: URI; @@ -58,15 +56,13 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi @IInstantiationService private instantiationService: IInstantiationService, @IStorageService private storageService: IStorageService ) { - this.toUnbind = []; + super(); + this.messages = new ResourceMap(); this.conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService); - const provider = instantiationService.createInstance(FileOnDiskContentProvider); - this.toUnbind.push(provider); - - const registrationDisposal = textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider); - this.toUnbind.push(registrationDisposal); + const provider = this._register(instantiationService.createInstance(FileOnDiskContentProvider)); + this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); // Hook into model TextFileEditorModel.setSaveErrorHandler(this); @@ -75,9 +71,9 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi } private registerListeners(): void { - this.toUnbind.push(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource))); - this.toUnbind.push(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource))); - this.toUnbind.push(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); + this._register(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource))); + this._register(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource))); + this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); } private onActiveEditorChanged(): void { @@ -105,7 +101,7 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi } } - public onSaveError(error: any, model: ITextFileEditorModel): void { + onSaveError(error: any, model: ITextFileEditorModel): void { const fileOperationError = error as FileOperationError; const resource = model.getResource(); @@ -181,8 +177,8 @@ export class SaveErrorHandler implements ISaveErrorHandler, IWorkbenchContributi this.messages.set(model.getResource(), handle); } - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); + dispose(): void { + super.dispose(); this.messages.clear(); } @@ -203,7 +199,7 @@ class ResolveConflictLearnMoreAction extends Action { super('workbench.files.action.resolveConflictLearnMore', nls.localize('learnMore', "Learn More")); } - public run(): TPromise { + run(): TPromise { return this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=868264')); } } @@ -216,7 +212,7 @@ class DoNotShowResolveConflictLearnMoreAction extends Action { super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again")); } - public run(notification: IDisposable): TPromise { + run(notification: IDisposable): TPromise { this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true); // Hide notification @@ -239,7 +235,7 @@ class ResolveSaveConflictAction extends Action { super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare")); } - public run(): TPromise { + run(): TPromise { if (!this.model.isDisposed()) { const resource = this.model.getResource(); const name = paths.basename(resource.fsPath); @@ -281,12 +277,12 @@ class SaveElevatedAction extends Action { super('workbench.files.action.saveElevated', triedToMakeWriteable ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('saveElevated', "Retry as Admin...")); } - public run(): TPromise { + run(): TPromise { if (!this.model.isDisposed()) { this.model.save({ writeElevated: true, overwriteReadonly: this.triedToMakeWriteable - }).done(null, errors.onUnexpectedError); + }); } return TPromise.as(true); @@ -301,9 +297,9 @@ class OverwriteReadonlyAction extends Action { super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite")); } - public run(): TPromise { + run(): TPromise { if (!this.model.isDisposed()) { - this.model.save({ overwriteReadonly: true }).done(null, errors.onUnexpectedError); + this.model.save({ overwriteReadonly: true }); } return TPromise.as(true); diff --git a/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts b/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts index acee79dc764..4622377d98b 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/emptyView.ts @@ -11,7 +11,6 @@ import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; import { Button } from 'vs/base/browser/ui/button/button'; -import { $, Builder } from 'vs/base/browser/builder'; import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,7 +22,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { ResourcesDropHandler } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; export class EmptyView extends ViewletPanel { @@ -31,8 +32,8 @@ export class EmptyView extends ViewletPanel { public static readonly NAME = nls.localize('noWorkspace', "No Folder Opened"); private button: Button; - private messageDiv: Builder; - private titleDiv: Builder; + private messageElement: HTMLElement; + private titleElement: HTMLElement; constructor( options: IViewletViewOptions, @@ -48,23 +49,32 @@ export class EmptyView extends ViewletPanel { } public renderHeader(container: HTMLElement): void { - this.titleDiv = $('span').text(name).appendTo($('div.title').appendTo(container)); + const titleContainer = document.createElement('div'); + DOM.addClass(titleContainer, 'title'); + container.appendChild(titleContainer); + + this.titleElement = document.createElement('span'); + this.titleElement.textContent = name; + titleContainer.appendChild(this.titleElement); } protected renderBody(container: HTMLElement): void { DOM.addClass(container, 'explorer-empty-view'); - this.messageDiv = $('p').appendTo($('div.section').appendTo(container)); + const messageContainer = document.createElement('div'); + DOM.addClass(messageContainer, 'section'); + container.appendChild(messageContainer); - let section = $('div.section').appendTo(container); + this.messageElement = document.createElement('p'); + messageContainer.appendChild(this.messageElement); - this.button = new Button(section.getHTMLElement()); + this.button = new Button(messageContainer); attachButtonStyler(this.button, this.themeService); this.disposables.push(this.button.onDidClick(() => { const actionClass = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? AddRootFolderAction : env.isMacintosh ? OpenFileFolderAction : OpenFolderAction; const action = this.instantiationService.createInstance(actionClass, actionClass.ID, actionClass.LABEL); - this.actionRunner.run(action).done(() => { + this.actionRunner.run(action).then(() => { action.dispose(); }, err => { action.dispose(); @@ -72,9 +82,24 @@ export class EmptyView extends ViewletPanel { }); })); - this.disposables.push(DOM.addDisposableListener(container, DOM.EventType.DROP, (e: DragEvent) => { - const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); - dropHandler.handleDrop(e, () => undefined, targetGroup => undefined); + this.disposables.push(new DragAndDropObserver(container, { + onDrop: e => { + container.style.backgroundColor = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND).toString(); + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); + dropHandler.handleDrop(e, () => undefined, targetGroup => undefined); + }, + onDragEnter: (e) => { + container.style.backgroundColor = this.themeService.getTheme().getColor(listDropBackground).toString(); + }, + onDragEnd: () => { + container.style.backgroundColor = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND).toString(); + }, + onDragLeave: () => { + container.style.backgroundColor = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND).toString(); + }, + onDragOver: e => { + e.dataTransfer.dropEffect = 'copy'; + } })); this.setLabels(); @@ -82,17 +107,17 @@ export class EmptyView extends ViewletPanel { private setLabels(): void { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - this.messageDiv.text(nls.localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.")); + this.messageElement.textContent = nls.localize('noWorkspaceHelp', "You have not yet added a folder to the workspace."); if (this.button) { this.button.label = nls.localize('addFolder', "Add Folder"); } - this.titleDiv.text(this.contextService.getWorkspace().name); + this.titleElement.textContent = EmptyView.NAME; } else { - this.messageDiv.text(nls.localize('noFolderHelp', "You have not yet opened a folder.")); + this.messageElement.textContent = nls.localize('noFolderHelp', "You have not yet opened a folder."); if (this.button) { this.button.label = nls.localize('openFolder', "Open Folder"); } - this.titleDiv.text(this.title); + this.titleElement.textContent = this.title; } } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts index dcbb387a155..cc7f93492aa 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerDecorationsProvider.ts @@ -5,34 +5,42 @@ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { Model } from 'vs/workbench/parts/files/common/explorerModel'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IDisposable } from 'vscode-xterm'; +import { dispose } from 'vs/base/common/lifecycle'; export class ExplorerDecorationsProvider implements IDecorationsProvider { readonly label: string = localize('label', "Explorer"); private _onDidChange = new Emitter(); + private toDispose: IDisposable[]; constructor( private model: Model, @IWorkspaceContextService contextService: IWorkspaceContextService ) { - contextService.onDidChangeWorkspaceFolders(e => { + this.toDispose = []; + this.toDispose.push(contextService.onDidChangeWorkspaceFolders(e => { this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri)); - }); + })); } get onDidChange(): Event { return this._onDidChange.event; } + changed(uris: URI[]): void { + this._onDidChange.fire(uris); + } + provideDecorations(resource: URI): IDecorationData { const fileStat = this.model.findClosest(resource); - if (fileStat && fileStat.nonexistentRoot) { + if (fileStat && fileStat.isRoot && fileStat.isError) { return { tooltip: localize('canNotResolve', "Can not resolve workspace folder"), letter: '!', @@ -48,4 +56,8 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { return undefined; } + + dispose(): IDisposable[] { + return dispose(this.toDispose); + } } 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 d9507f86bec..5540ecb71a2 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -6,15 +6,14 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ThrottledDelayer, Delayer } from 'vs/base/common/async'; -import * as errors from 'vs/base/common/errors'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; import * as glob from 'vs/base/common/glob'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext } from 'vs/workbench/parts/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder, IExplorerView, ExplorerRootContext, ExplorerResourceReadonlyContext } from 'vs/workbench/parts/files/common/files'; import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileService, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files'; import { RefreshViewExplorerAction, NewFolderAction, NewFileAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { FileDragAndDrop, FileFilter, FileSorter, FileController, FileRenderer, FileDataSource, FileViewletState, FileAccessibilityProvider } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer'; @@ -43,6 +42,7 @@ import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { ILabelService } from 'vs/platform/label/common/label'; export interface IExplorerViewOptions extends IViewletViewOptions { viewletState: FileViewletState; @@ -67,6 +67,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView private resourceContext: ResourceContextKey; private folderContext: IContextKey; + private readonlyContext: IContextKey; private rootContext: IContextKey; private fileEventsFilter: ResourceGlobMatcher; @@ -77,6 +78,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView private settings: object; private treeContainer: HTMLElement; private dragHandler: DelayedDragHandler; + private decorationProvider: ExplorerDecorationsProvider; private isDisposed: boolean; constructor( @@ -92,7 +94,8 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, - @IDecorationsService decorationService: IDecorationsService + @IDecorationsService decorationService: IDecorationsService, + @ILabelService private labelService: ILabelService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); @@ -104,6 +107,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView this.resourceContext = instantiationService.createInstance(ResourceContextKey); this.folderContext = ExplorerFolderContext.bindTo(contextKeyService); + this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService); this.rootContext = ExplorerRootContext.bindTo(contextKeyService); this.fileEventsFilter = instantiationService.createInstance( @@ -112,7 +116,10 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) ); - decorationService.registerDecorationsProvider(new ExplorerDecorationsProvider(this.model, contextService)); + this.decorationProvider = new ExplorerDecorationsProvider(this.model, contextService); + decorationService.registerDecorationsProvider(this.decorationProvider); + this.disposables.push(this.decorationProvider); + this.disposables.push(this.resourceContext); } private getFileEventsExcludes(root?: URI): glob.IExpression { @@ -137,11 +144,12 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }; this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader)); + this.disposables.push(this.labelService.onDidRegisterFormatter(setHeader)); setHeader(); } public get name(): string { - return this.contextService.getWorkspace().name; + return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace()); } public get title(): string { @@ -191,6 +199,11 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.refreshFromEvent(e.added))); this.disposables.push(this.contextService.onDidChangeWorkbenchState(e => this.refreshFromEvent())); + this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this.refreshFromEvent())); + this.disposables.push(this.labelService.onDidRegisterFormatter(() => { + this._onDidChangeTitleArea.fire(); + this.refreshFromEvent(); + })); } layoutBody(size: number): void { @@ -230,7 +243,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView if (this.isVisible() && !this.isDisposed && this.contextService.isInsideWorkspace(activeFile)) { const selection = this.hasSingleSelection(activeFile); if (!selection) { - this.select(activeFile).done(null, errors.onUnexpectedError); + this.select(activeFile); } clearSelection = false; @@ -280,7 +293,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView // Refresh viewer as needed if this originates from a config event if (event && needsRefresh) { - this.doRefresh().done(null, errors.onUnexpectedError); + this.doRefresh(); } } @@ -294,7 +307,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView if (this.autoReveal) { const selection = this.explorerViewer.getSelection(); if (selection.length > 0) { - this.reveal(selection[0], 0.5).done(null, errors.onUnexpectedError); + this.reveal(selection[0], 0.5); } } @@ -433,6 +446,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const resource = e.focus ? e.focus.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : undefined; this.resourceContext.set(resource); this.folderContext.set((isSingleFolder && !e.focus) || e.focus && e.focus.isDirectory); + this.readonlyContext.set(e.focus && e.focus.isReadonly); this.rootContext.set(!e.focus || (e.focus && e.focus.isRoot)); })); @@ -497,7 +511,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView // Focus new element this.explorerViewer.setFocus(childElement); }); - }).done(null, errors.onUnexpectedError); + }); }); }); } @@ -529,7 +543,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView modelElement.rename(newElement); // Update Parent (View) - this.explorerViewer.refresh(modelElement.parent).done(() => { + this.explorerViewer.refresh(modelElement.parent).then(() => { // Select in Viewer if set if (restoreFocus) { @@ -539,7 +553,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView if (isExpanded) { this.explorerViewer.expand(modelElement); } - }, errors.onUnexpectedError); + }); }); } @@ -555,10 +569,10 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView const oldParent = modelElement.parent; modelElement.move(newParents[index], (callback: () => void) => { // Update old parent - this.explorerViewer.refresh(oldParent).done(callback, errors.onUnexpectedError); + this.explorerViewer.refresh(oldParent).then(callback); }, () => { // Update new parent - this.explorerViewer.refresh(newParents[index], true).done(() => this.explorerViewer.expand(newParents[index]), errors.onUnexpectedError); + this.explorerViewer.refresh(newParents[index], true).then(() => this.explorerViewer.expand(newParents[index])); }); }); } @@ -576,13 +590,13 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView // Refresh Parent (View) const restoreFocus = this.explorerViewer.isDOMFocused(); - this.explorerViewer.refresh(parent).done(() => { + this.explorerViewer.refresh(parent).then(() => { // Ensure viewer has keyboard focus if event originates from viewer if (restoreFocus) { this.explorerViewer.domFocus(); } - }, errors.onUnexpectedError); + }); } }); } @@ -711,7 +725,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } return TPromise.as(null); - }).done(null, errors.onUnexpectedError); + }); } else { this.shouldRefresh = true; } @@ -779,8 +793,11 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }); } - const promise = this.resolveRoots(targetsToResolve, targetsToExpand); - this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */); + const promise = this.resolveRoots(targetsToResolve, targetsToExpand).then(result => { + this.decorationProvider.changed(targetsToResolve.map(t => t.root.resource)); + return result; + }); + this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 1200 /* less ugly initial startup */); return promise; } @@ -788,14 +805,21 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView private resolveRoots(targetsToResolve: { root: ExplorerItem, resource: URI, options: { resolveTo: any[] } }[], targetsToExpand: URI[]): TPromise { // Display roots only when multi folder workspace - const input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model; - const errorFileStat = (resource: URI, root: ExplorerItem) => ExplorerItem.create({ - resource: resource, - name: paths.basename(resource.fsPath), - mtime: 0, - etag: undefined, - isDirectory: true - }, root); + let input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model; + + const errorRoot = (resource: URI, root: ExplorerItem) => { + if (input === this.model.roots[0]) { + input = this.model; + } + + return ExplorerItem.create({ + resource: resource, + name: paths.basename(resource.fsPath), + mtime: 0, + etag: undefined, + isDirectory: true + }, root, undefined, true); + }; const setInputAndExpand = (input: ExplorerItem | Model, statsToExpand: ExplorerItem[]) => { // Make sure to expand all folders that where expanded in the previous session @@ -816,7 +840,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return ExplorerItem.create(result.stat, targetsToResolve[index].root, targetsToResolve[index].options.resolveTo); } - return errorFileStat(targetsToResolve[index].resource, targetsToResolve[index].root); + return errorRoot(targetsToResolve[index].resource, targetsToResolve[index].root); }); // Subsequent refresh: Merge stat into our local model and refresh tree modelStats.forEach((modelStat, index) => { @@ -839,7 +863,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView let delayer = new Delayer(100); let delayerPromise: TPromise; return TPromise.join(targetsToResolve.map((target, index) => this.fileService.resolveFile(target.resource, target.options) - .then(result => result.isDirectory ? ExplorerItem.create(result, target.root, target.options.resolveTo) : errorFileStat(target.resource, target.root), err => errorFileStat(target.resource, target.root)) + .then(result => result.isDirectory ? ExplorerItem.create(result, target.root, target.options.resolveTo) : errorRoot(target.resource, target.root), () => errorRoot(target.resource, target.root)) .then(modelStat => { // Subsequent refresh: Merge stat into our local model and refresh tree if (index < this.model.roots.length) { 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 435acd5331e..7368ff7e693 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import * as DOM from 'vs/base/browser/dom'; import * as path from 'path'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { once } from 'vs/base/common/functional'; import * as paths from 'vs/base/common/paths'; import * as resources from 'vs/base/common/resources'; @@ -20,7 +20,7 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; import * as glob from 'vs/base/common/glob'; import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; @@ -56,7 +56,7 @@ import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class FileDataSource implements IDataSource { constructor( @@ -233,7 +233,7 @@ export class FileRenderer implements IRenderer { } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData { - const elementDisposable = EmptyDisposable; + const elementDisposable = Disposable.None; const label = this.instantiationService.createInstance(FileLabel, container, void 0); return { elementDisposable, label, container }; @@ -264,7 +264,7 @@ export class FileRenderer implements IRenderer { else { templateData.label.element.style.display = 'none'; this.renderInputBox(templateData.container, tree, stat, editableData); - templateData.elementDisposable = EmptyDisposable; + templateData.elementDisposable = Disposable.None; } } @@ -275,7 +275,11 @@ export class FileRenderer implements IRenderer { const extraClasses = ['explorer-item', 'explorer-item-edited']; const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : (stat.isDirectory || (stat instanceof NewStatPlaceholder && stat.isDirectoryPlaceholder())) ? FileKind.FOLDER : FileKind.FILE; const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses }; - label.setFile(stat.resource, labelOptions); + + const parent = stat.name ? resources.dirname(stat.resource) : stat.resource; + const value = stat.name || ''; + + label.setFile(resources.joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty. // Input field for name const inputBox = new InputBox(label.element, this.contextViewService, { @@ -286,12 +290,10 @@ export class FileRenderer implements IRenderer { }); const styler = attachInputBoxStyler(inputBox, this.themeService); - const parent = resources.dirname(stat.resource); inputBox.onDidChange(value => { - label.setFile(parent.with({ path: paths.join(parent.path, value) }), labelOptions); // update label icon while typing! + label.setFile(resources.joinPath(parent, value || ' '), labelOptions); // update label icon while typing! }); - const value = stat.name || ''; const lastDot = value.lastIndexOf('.'); inputBox.value = value; @@ -332,7 +334,7 @@ export class FileRenderer implements IRenderer { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { projectFolderName = paths.basename(stat.root.resource.path); // show root folder name in multi-folder project } - this.displayCurrentPath(inputBox, initialRelPath, projectFolderName, editableData.action.id); + this.showInputMessage(inputBox, initialRelPath, projectFolderName, editableData.action.id); }), DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => { done(inputBox.isInputValid(), true); @@ -342,7 +344,7 @@ export class FileRenderer implements IRenderer { ]; } - private displayCurrentPath(inputBox: InputBox, initialRelPath: string, projectFolderName: string = '', actionID: string) { + private showInputMessage(inputBox: InputBox, initialRelPath: string, projectFolderName: string = '', actionID: string) { if (inputBox.validate()) { const value = inputBox.value; if (value && /.[\\/]./.test(value)) { // only show if there's at least one slash enclosed in the string @@ -371,8 +373,13 @@ export class FileRenderer implements IRenderer { content: msg, formatContent: true }); - } - else { // fixes #46744: inputbox hides again if all slashes are removed + } else if (value && /^\s|\s$/.test(value)) { + inputBox.showMessage({ + content: nls.localize('whitespace', "Leading or trailing whitespace detected"), + formatContent: true, + type: MessageType.WARNING + }); + } else { // fixes #46744: inputbox hides again if all slashes are removed inputBox.hideMessage(); } } @@ -383,7 +390,7 @@ export class FileRenderer implements IRenderer { export class FileAccessibilityProvider implements IAccessibilityProvider { public getAriaLabel(tree: ITree, stat: ExplorerItem): string { - return nls.localize('filesExplorerViewerAriaLabel', "{0}, Files Explorer", stat.name); + return stat.name; } } @@ -401,6 +408,7 @@ export class FileController extends WorkbenchTreeController implements IDisposab @IMenuService private menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService private clipboardService: IClipboardService, + @IKeybindingService private keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService ) { super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */ }, configurationService); @@ -494,7 +502,7 @@ export class FileController extends WorkbenchTreeController implements IDisposab sideBySide = tree.useAltAsMultipleSelectionModifier ? (event.ctrlKey || event.metaKey) : event.altKey; } - this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick }); + this.openEditor(stat, { preserveFocus, sideBySide, pinned: isDoubleClick || (event && event.middleButton) }); } } @@ -533,6 +541,9 @@ export class FileController extends WorkbenchTreeController implements IDisposab tree.domFocus(); } }, + getKeyBinding: (action) => { + return this.keybindingService.lookupKeybinding(action.id); + }, getActionsContext: () => selection && selection.indexOf(stat) >= 0 ? selection.map((fs: ExplorerItem) => fs.resource) : stat instanceof ExplorerItem ? [stat.resource] : [] @@ -674,18 +685,21 @@ export class FileSorter implements ISorter { } // Explorer Filter +interface CachedParsedExpression { + original: glob.IExpression; + parsed: glob.ParsedExpression; +} + export class FileFilter implements IFilter { - private static readonly MAX_SIBLINGS_FILTER_THRESHOLD = 2000; - - private hiddenExpressionPerRoot: Map; + private hiddenExpressionPerRoot: Map; private workspaceFolderChangeListener: IDisposable; constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService ) { - this.hiddenExpressionPerRoot = new Map(); + this.hiddenExpressionPerRoot = new Map(); this.registerListeners(); } @@ -698,9 +712,16 @@ export class FileFilter implements IFilter { let needsRefresh = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue({ resource: folder.uri }); - const excludesConfig = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); - needsRefresh = needsRefresh || !objects.equals(this.hiddenExpressionPerRoot.get(folder.uri.toString()), excludesConfig); - this.hiddenExpressionPerRoot.set(folder.uri.toString(), objects.deepClone(excludesConfig)); // do not keep the config, as it gets mutated under our hoods + const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null); + + if (!needsRefresh) { + const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); + needsRefresh = !cached || !objects.equals(cached.original, excludesConfig); + } + + const excludesConfigCopy = objects.deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods + + this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression); }); return needsRefresh; @@ -715,18 +736,9 @@ export class FileFilter implements IFilter { return true; // always visible } - // Workaround for O(N^2) complexity (https://github.com/Microsoft/vscode/issues/9962) - let siblingsFn: () => string[]; - let siblingCount = stat.parent && stat.parent.getChildrenCount(); - if (siblingCount && siblingCount > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) { - siblingsFn = () => void 0; - } else { - siblingsFn = () => stat.parent ? stat.parent.getChildrenNames() : void 0; - } - // Hide those that match Hidden Patterns - const expression = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()) || Object.create(null); - if (glob.match(expression, paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), siblingsFn)) { + const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); + if (cached && cached.parsed(paths.normalize(path.relative(stat.root.resource.path, stat.resource.path), true), stat.name, name => !!stat.parent.getChild(name))) { return false; // hidden through pattern } @@ -754,7 +766,6 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @ITextFileService private textFileService: ITextFileService, - @IBackupFileService private backupFileService: IBackupFileService, @IWindowService private windowService: IWindowService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService ) { @@ -860,6 +871,11 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { return true; // Can not move anything onto itself } + if (source.isRoot && target instanceof ExplorerItem && target.isRoot) { + // Disable moving workspace roots in one another + return false; + } + if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) { return true; // Can not move a file to the same parent unless we copy } @@ -882,6 +898,9 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { // All (target = file/folder) else { if (target.isDirectory) { + if (target.isReadonly) { + return DRAG_OVER_REJECT; + } return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true); } @@ -894,19 +913,16 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { } public drop(tree: ITree, data: IDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): void { - let promise: TPromise = TPromise.as(null); // Desktop DND (Import file) if (data instanceof DesktopDragAndDropData) { - promise = this.handleExternalDrop(tree, data, target, originalEvent); + this.handleExternalDrop(tree, data, target, originalEvent); } // In-Explorer DND (Move/Copy file) else { - promise = this.handleExplorerDrop(tree, data, target, originalEvent); + this.handleExplorerDrop(tree, data, target, originalEvent); } - - promise.done(null, errors.onUnexpectedError); } private handleExternalDrop(tree: ITree, data: DesktopDragAndDropData, target: ExplorerItem | Model, originalEvent: DragMouseEvent): TPromise { @@ -942,7 +958,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { } // Handle dropped files (only support FileStat as target) - else if (target instanceof ExplorerItem) { + else if (target instanceof ExplorerItem && !target.isReadonly) { const addFilesAction = this.instantiationService.createInstance(AddFilesAction, tree, target, null); return addFilesAction.run(droppedResources.map(res => res.resource)); @@ -1028,91 +1044,52 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { } private doHandleExplorerDrop(tree: ITree, source: ExplorerItem, target: ExplorerItem | Model, isCopy: boolean): TPromise { + if (!(target instanceof ExplorerItem)) { + return TPromise.as(void 0); + } + return tree.expand(target).then(() => { + + if (target.isReadonly) { + return void 0; + } + // Reuse duplicate action if user copies if (isCopy) { return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run(); } - const dirtyMoved: URI[] = []; + // Otherwise move + const targetResource = resources.joinPath(target.resource, source.name); - // Success: load all files that are dirty again to restore their dirty contents - // Error: discard any backups created during the process - const onSuccess = () => TPromise.join(dirtyMoved.map(t => this.textFileService.models.loadOrCreate(t))); - const onError = (error?: Error, showError?: boolean) => { - if (showError) { + return this.textFileService.move(source.resource, targetResource).then(null, error => { + + // Conflict + if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { + const confirm: IConfirmation = { + message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), + detail: nls.localize('irreversible', "This action is irreversible!"), + primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + // Move with overwrite if the user confirms + return this.dialogService.confirm(confirm).then(res => { + if (res.confirmed) { + return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(null, error => this.notificationService.error(error)); + } + + return void 0; + }); + } + + // Any other error + else { this.notificationService.error(error); } - return TPromise.join(dirtyMoved.map(d => this.backupFileService.discardResourceBackup(d))); - }; - if (!(target instanceof ExplorerItem)) { - return TPromise.as(void 0); - } - - // 1. check for dirty files that are being moved and backup to new target - const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, source.resource, !isLinux /* ignorecase */)); - return TPromise.join(dirty.map(d => { - let moved: URI; - - // If the dirty file itself got moved, just reparent it to the target folder - if (source.resource.toString() === d.toString()) { - moved = target.resource.with({ path: paths.join(target.resource.path, source.name) }); - } - - // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: - else { - moved = target.resource.with({ path: paths.join(target.resource.path, d.path.substr(source.parent.resource.path.length + 1)) }); - } - - dirtyMoved.push(moved); - - const model = this.textFileService.models.get(d); - - return this.backupFileService.backupResource(moved, model.createSnapshot(), model.getVersionId()); - })) - - // 2. soft revert all dirty since we have backed up their contents - .then(() => this.textFileService.revertAll(dirty, { soft: true /* do not attempt to load content from disk */ })) - - // 3.) run the move operation - .then(() => { - const targetResource = target.resource.with({ path: paths.join(target.resource.path, source.name) }); - - return this.fileService.moveFile(source.resource, targetResource).then(null, error => { - - // Conflict - if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - // Move with overwrite if the user confirms - return this.dialogService.confirm(confirm).then(res => { - if (res.confirmed) { - const targetDirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, targetResource, !isLinux /* ignorecase */)); - - // Make sure to revert all dirty in target first to be able to overwrite properly - return this.textFileService.revertAll(targetDirty, { soft: true /* do not attempt to load content from disk */ }).then(() => { - - // Then continue to do the move operation - return this.fileService.moveFile(source.resource, targetResource, true).then(onSuccess, error => onError(error, true)); - }); - } - - return onError(); - }); - } - - return onError(error, true); - }); - }) - - // 4.) resolve those that were dirty to load their previous dirty contents from disk - .then(onSuccess, onError); + return void 0; + }); }, errors.onUnexpectedError); } } 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 aac21c37e05..454bb86bce6 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IAction, ActionRunner } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/group/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorInput } from 'vs/workbench/common/editor'; @@ -26,7 +25,7 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { EditorLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -37,7 +36,7 @@ import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemAc import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { DirtyEditorContext, OpenEditorsGroupContext } from 'vs/workbench/parts/files/electron-browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { fillResourceDataTransfers, ResourcesDropHandler, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { fillResourceDataTransfers, ResourcesDropHandler, LocalSelectionTransfer, CodeDataTransfers } from 'vs/workbench/browser/dnd'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -160,6 +159,7 @@ export class OpenEditorsView extends ViewletPanel { } } })); + this.disposables.push(groupDisposables.get(group.id)); }; this.editorGroupService.groups.forEach(g => addGroupListener(g)); @@ -175,8 +175,7 @@ export class OpenEditorsView extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const title = dom.append(container, $('.title')); - dom.append(title, $('span', null, this.title)); + super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); this.dirtyCountElement = dom.append(count, $('.monaco-count-badge')); @@ -211,6 +210,10 @@ export class OpenEditorsView extends ViewletPanel { return focused; }; + + if (this.list) { + this.list.dispose(); + } this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [ new EditorGroupRenderer(this.keybindingService, this.instantiationService, this.editorGroupService), new OpenEditorRenderer(getSelectedElements, this.instantiationService, this.keybindingService, this.configurationService, this.editorGroupService) @@ -218,6 +221,7 @@ export class OpenEditorsView extends ViewletPanel { identityProvider: (element: OpenEditor | IEditorGroup) => element instanceof OpenEditor ? element.getId() : element.id.toString(), selectOnMouseDown: false /* disabled to better support DND */ }) as WorkbenchList; + this.disposables.push(this.list); this.contributedContextMenu = this.menuService.createMenu(MenuId.OpenEditorsContext, this.list.contextKeyService); this.disposables.push(this.contributedContextMenu); @@ -229,6 +233,7 @@ export class OpenEditorsView extends ViewletPanel { ExplorerFocusedContext.bindTo(this.list.contextKeyService); this.resourceContext = this.instantiationService.createInstance(ResourceContextKey); + this.disposables.push(this.resourceContext); this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService); this.dirtyEditorFocusedContext = DirtyEditorContext.bindTo(this.contextKeyService); @@ -265,7 +270,7 @@ export class OpenEditorsView extends ViewletPanel { const element = focused.length ? focused[0] : undefined; if (element instanceof OpenEditor) { if (isMiddleClick) { - element.group.closeEditor(element.editor).done(null, errors.onUnexpectedError); + element.group.closeEditor(element.editor); } else { this.openEditor(element, { preserveFocus: isSingleClick, pinned: isDoubleClick, sideBySide: openToSide }); } @@ -287,6 +292,7 @@ export class OpenEditorsView extends ViewletPanel { public setExpanded(expanded: boolean): void { super.setExpanded(expanded); + this.updateListVisibility(expanded); if (expanded && this.needsRefresh) { this.listRefreshScheduler.schedule(0); } @@ -294,6 +300,7 @@ export class OpenEditorsView extends ViewletPanel { public setVisible(visible: boolean): TPromise { return super.setVisible(visible).then(() => { + this.updateListVisibility(visible && this.isExpanded()); if (visible && this.needsRefresh) { this.listRefreshScheduler.schedule(0); } @@ -315,13 +322,23 @@ export class OpenEditorsView extends ViewletPanel { } } + private updateListVisibility(isVisible: boolean): void { + if (this.list) { + if (isVisible) { + dom.show(this.list.getHTMLElement()); + } else { + dom.hide(this.list.getHTMLElement()); // make sure the list goes out of the tabindex world by hiding it + } + } + } + private get showGroups(): boolean { return this.editorGroupService.groups.length > 1; } private get elements(): (IEditorGroup | OpenEditor)[] { const result: (IEditorGroup | OpenEditor)[] = []; - this.editorGroupService.groups.forEach(g => { + this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(g => { if (this.showGroups) { result.push(g); } @@ -337,7 +354,7 @@ export class OpenEditorsView extends ViewletPanel { return index; } - for (let g of this.editorGroupService.groups) { + for (let g of this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)) { if (g.id === group.id) { return index + (!!editor ? 1 : 0); } else { @@ -362,11 +379,11 @@ export class OpenEditorsView extends ViewletPanel { if (!preserveActivateGroup) { this.editorGroupService.activateGroup(element.groupId); // needed for https://github.com/Microsoft/vscode/issues/6672 } - this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP).done(editor => { + this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { if (!preserveActivateGroup) { this.editorGroupService.activateGroup(editor.group); } - }, errors.onUnexpectedError); + }); } } @@ -384,7 +401,7 @@ export class OpenEditorsView extends ViewletPanel { } private focusActiveEditor(): void { - if (this.editorGroupService.activeGroup && this.editorGroupService.activeGroup.activeEditor /* could be empty */) { + 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]); @@ -483,7 +500,7 @@ class OpenEditorActionRunner extends ActionRunner { } } -class OpenEditorsDelegate implements IDelegate { +class OpenEditorsDelegate implements IVirtualDelegate { public static readonly ITEM_HEIGHT = 22; @@ -500,6 +517,32 @@ class OpenEditorsDelegate implements IDelegate { } } +/** + * Check if the item being dragged is one of the supported types that can be dropped on an + * open editor or editor group. Fixes https://github.com/Microsoft/vscode/issues/52344. + * @param e + * @returns true if dropping is supported. + */ +function dropOnEditorSupported(e: DragEvent): boolean { + // DataTransfer types are automatically converted to lower case, except Files. + const supportedTransferTypes = { + openEditor: CodeDataTransfers.EDITORS.toLowerCase(), + externalFile: 'Files', + codeFile: CodeDataTransfers.FILES.toLowerCase() + }; + + if ( + e.dataTransfer.types.indexOf(supportedTransferTypes.openEditor) !== -1 || + e.dataTransfer.types.indexOf(supportedTransferTypes.externalFile) !== -1 || + // All Code files should already register as normal files, but just to be safe: + e.dataTransfer.types.indexOf(supportedTransferTypes.codeFile) !== -1 + ) { + return true; + } else { + return false; + } +} + class EditorGroupRenderer implements IRenderer { static readonly ID = 'editorgroup'; @@ -532,8 +575,10 @@ class EditorGroupRenderer implements IRenderer { - dom.addClass(container, 'focused'); + editorGroupTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_OVER, (e: DragEvent) => { + if (dropOnEditorSupported(e)) { + dom.addClass(container, 'focused'); + } })); editorGroupTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_LEAVE, () => { dom.removeClass(container, 'focused'); @@ -561,6 +606,10 @@ class EditorGroupRenderer implements IRenderer d.getResource()), e); } })); - editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_OVER, () => { - dom.addClass(container, 'focused'); + editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_OVER, (e: DragEvent) => { + if (dropOnEditorSupported(e)) { + dom.addClass(container, 'focused'); + } })); editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_LEAVE, () => { dom.removeClass(container, 'focused'); @@ -655,6 +706,10 @@ class OpenEditorRenderer implements IRenderer { const inputToResolve: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/file.js'), void 0); const sameOtherInput: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/file.js'), void 0); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert.ok(inputToResolve.isResolved()); const resolvedModelA = resolved; - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA === resolved); // OK: Resolved Model cached globally per input - return sameOtherInput.resolve(true).then(otherResolved => { + return sameOtherInput.resolve().then(otherResolved => { assert(otherResolved === resolvedModelA); // OK: Resolved Model cached globally per input inputToResolve.dispose(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA === resolved); // Model is still the same because we had 2 clients inputToResolve.dispose(); @@ -83,17 +83,12 @@ suite('Files - FileEditorInput', () => { resolvedModelA.dispose(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(resolvedModelA !== resolved); // Different instance, because input got disposed let stat = (resolved as TextFileEditorModel).getStat(); - return inputToResolve.resolve(true).then(resolved => { + return inputToResolve.resolve().then(resolved => { assert(stat !== (resolved as TextFileEditorModel).getStat()); // Different stat, because resolve always goes to the server for refresh - - stat = (resolved as TextFileEditorModel).getStat(); - return inputToResolve.resolve(false).then(resolved => { - assert(stat === (resolved as TextFileEditorModel).getStat()); // Same stat, because not refreshed - }); }); }); }); @@ -122,7 +117,7 @@ suite('Files - FileEditorInput', () => { input.setEncoding('utf16', EncodingMode.Encode); assert.equal(input.getEncoding(), 'utf16'); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { assert.equal(input.getEncoding(), resolved.getEncoding()); resolved.dispose(); @@ -132,7 +127,7 @@ suite('Files - FileEditorInput', () => { test('save', function () { const input = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/updatefile.js'), void 0); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { resolved.textEditorModel.setValue('changed'); assert.ok(input.isDirty()); @@ -147,7 +142,7 @@ suite('Files - FileEditorInput', () => { test('revert', function () { const input = instantiationService.createInstance(FileEditorInput, toResource(this, '/foo/bar/updatefile.js'), void 0); - return input.resolve(true).then((resolved: TextFileEditorModel) => { + return input.resolve().then((resolved: TextFileEditorModel) => { resolved.textEditorModel.setValue('changed'); assert.ok(input.isDirty()); @@ -164,7 +159,7 @@ suite('Files - FileEditorInput', () => { accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_IS_BINARY)); - return input.resolve(true).then(resolved => { + return input.resolve().then(resolved => { assert.ok(resolved); resolved.dispose(); @@ -176,7 +171,7 @@ suite('Files - FileEditorInput', () => { accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_TOO_LARGE)); - return input.resolve(true).then(resolved => { + return input.resolve().then(resolved => { assert.ok(resolved); resolved.dispose(); diff --git a/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts index b3443b8850b..d9142f4d9f0 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { FileEditorTracker } from 'vs/workbench/parts/files/browser/editors/fileEditorTracker'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestTextFileService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; 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 515ad42c580..a7f2a269eb2 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 @@ -8,17 +8,22 @@ import * as assert from 'assert'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; import { validateFileName } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { ExplorerItem } from 'vs/workbench/parts/files/common/explorerModel'; function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource(path), undefined, false, isFolder, name, mtime); + return new ExplorerItem(toResource(path), undefined, false, false, isFolder, name, mtime); } function toResource(path) { - return URI.file(join('C:\\', path)); + if (isWindows) { + return URI.file(join('C:\\', path)); + } else { + return URI.file(join('/home/john', path)); + } + } suite('Files - View Model', () => { @@ -264,20 +269,20 @@ suite('Files - View Model', () => { test('Merge Local with Disk', function () { const d = new Date().toUTCString(); - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, true, 'to', Date.now(), d); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, true, 'to', Date.now(), new Date(0).toUTCString()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, false, true, 'to', Date.now(), d); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, false, false, true, 'to', Date.now(), new Date(0).toUTCString()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, true, 'foo.html', Date.now(), d)); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d)); ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.getChildrenArray().length, 0); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, true, 'foo.html', Date.now(), d); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, false, true, 'foo.html', Date.now(), d); merge2.removeChild(child); merge2.addChild(child); merge2.isDirectoryResolved = true; diff --git a/src/vs/workbench/parts/html/common/htmlInput.ts b/src/vs/workbench/parts/html/common/htmlInput.ts index 7f20aa1588e..e70b08d999a 100644 --- a/src/vs/workbench/parts/html/common/htmlInput.ts +++ b/src/vs/workbench/parts/html/common/htmlInput.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; diff --git a/src/vs/workbench/parts/html/electron-browser/html.contribution.ts b/src/vs/workbench/parts/html/electron-browser/html.contribution.ts index 1ccfdc46891..cc53effa58f 100644 --- a/src/vs/workbench/parts/html/electron-browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/electron-browser/html.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -17,6 +17,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { registerWebViewCommands } from 'vs/workbench/parts/webview/electron-browser/webview.contribution'; function getActivePreviewsForResource(accessor: ServicesAccessor, resource: URI | string) { const uri = resource instanceof URI ? resource : URI.parse(resource); @@ -98,3 +99,5 @@ CommandsRegistry.registerCommand('_workbench.htmlPreview.postMessage', function } return activePreviews.length > 0; }); + +registerWebViewCommands(HtmlPreviewPart.ID); \ No newline at end of file diff --git a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts index cd17142a8d0..d61e2042a1a 100644 --- a/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/electron-browser/htmlPreviewPart.ts @@ -8,8 +8,8 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; -import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; -import { EditorOptions, EditorInput, EditorViewStateMemento } from 'vs/workbench/common/editor'; +import { Disposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; +import { EditorOptions, EditorInput, IEditorMemento } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { HtmlInput, HtmlInputOptions, areHtmlInputOptionsEqual } from 'vs/workbench/parts/html/common/htmlInput'; @@ -19,7 +19,6 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { Parts, IPartService } from 'vs/workbench/services/part/common/partService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { Scope } from 'vs/workbench/common/memento'; import { Dimension } from 'vs/base/browser/dom'; import { BaseWebviewEditor } from 'vs/workbench/parts/webview/electron-browser/baseWebviewEditor'; import { WebviewElement, WebviewOptions } from 'vs/workbench/parts/webview/electron-browser/webviewElement'; @@ -43,13 +42,13 @@ export class HtmlPreviewPart extends BaseWebviewEditor { private _modelRef: IReference; public get model(): ITextModel { return this._modelRef && this._modelRef.object.textEditorModel; } - private _modelChangeSubscription = EmptyDisposable; - private _themeChangeSubscription = EmptyDisposable; + private _modelChangeSubscription = Disposable.None; + private _themeChangeSubscription = Disposable.None; private _content: HTMLElement; private _scrollYPercentage: number = 0; - private editorViewStateMemento: EditorViewStateMemento; + private editorMemento: IEditorMemento; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -60,11 +59,11 @@ export class HtmlPreviewPart extends BaseWebviewEditor { @IStorageService readonly _storageService: IStorageService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService readonly editorGroupService: IEditorGroupsService ) { super(HtmlPreviewPart.ID, telemetryService, themeService, contextKeyService); - this.editorViewStateMemento = new EditorViewStateMemento(editorGroupService, this.getMemento(_storageService, Scope.WORKSPACE), this.viewStateStorageKey); + this.editorMemento = this.getEditorMemento(_storageService, editorGroupService, this.viewStateStorageKey); } dispose(): void { @@ -96,8 +95,6 @@ export class HtmlPreviewPart extends BaseWebviewEditor { this._webview = this._instantiationService.createInstance(WebviewElement, this._partService.getContainer(Parts.EDITOR_PART), - this.contextKey, - this.findInputFocusContextKey, { ...webviewOptions, useSameOriginForRoot: true @@ -247,18 +244,10 @@ export class HtmlPreviewPart extends BaseWebviewEditor { } private saveHTMLPreviewViewState(input: HtmlInput, editorViewState: HtmlPreviewEditorViewState): void { - this.editorViewStateMemento.saveState(this.group, input, editorViewState); + this.editorMemento.saveState(this.group, input, editorViewState); } private loadHTMLPreviewViewState(input: HtmlInput): HtmlPreviewEditorViewState { - return this.editorViewStateMemento.loadState(this.group, input); - } - - protected saveMemento(): void { - - // ensure to first save our view state memento - this.editorViewStateMemento.save(); - - super.saveMemento(); + return this.editorMemento.loadState(this.group, input); } } diff --git a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts index dbb24061980..f477e490598 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts @@ -12,7 +12,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import { ConfigureLocaleAction } from 'vs/workbench/parts/localizations/electron-browser/localizationsActions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ILocalizationsService, LanguageType } from 'vs/platform/localizations/common/localizations'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { IExtensionManagementService, DidInstallExtensionEvent, LocalExtensionType, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -20,7 +20,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import Severity from 'vs/base/common/severity'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -29,6 +29,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; import { minimumTranslatedStrings } from 'vs/platform/node/minimalTranslations'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { CancellationToken } from 'vs/base/common/cancellation'; // Register action to configure locale and related settings const registry = Registry.as(Extensions.WorkbenchActions); @@ -98,24 +99,6 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } } - private migrateToMarketplaceLanguagePack(language: string): void { - this.isLanguageInstalled(language) - .then(installed => { - if (!installed) { - this.getLanguagePackExtension(language) - .then(extension => { - if (extension) { - this.notificationService.prompt(Severity.Warning, localize('install language pack', "In the near future, VS Code will only support language packs in the form of Marketplace extensions. Please install the '{0}' extension in order to continue to use the currently configured language. ", extension.displayName || extension.displayName), - [ - { label: localize('install', "Install"), run: () => this.installExtension(extension) }, - { label: localize('more information', "More Information..."), run: () => window.open('https://go.microsoft.com/fwlink/?linkid=872941') } - ]); - } - }); - } - }); - } - private checkAndInstall(): void { const language = platform.language; const locale = platform.locale; @@ -124,11 +107,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo if (!this.galleryService.isEnabled()) { return; } - if (language !== 'en' && language !== 'en_us') { - this.migrateToMarketplaceLanguagePack(language); - return; - } - if (locale === 'en' || locale === 'en_us') { + if (language === 'en' || language.indexOf('en-') === 0) { return; } if (language === locale || languagePackSuggestionIgnoreList.indexOf(language) > -1) { @@ -141,29 +120,36 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo return; } - const ceintlExtensionSearch = this.galleryService.query({ names: [`MS-CEINTL.vscode-language-pack-${locale}`], pageSize: 1 }); - const tagSearch = this.galleryService.query({ text: `tag:lp-${locale}`, pageSize: 1 }); - - TPromise.join([ceintlExtensionSearch, tagSearch]).then(([ceintlResult, tagResult]) => { - if (ceintlResult.total === 0 && tagResult.total === 0) { + this.galleryService.query({ text: `tag:lp-${locale}` }).then(tagResult => { + if (tagResult.total === 0) { return; } - const extensionToInstall = ceintlResult.total === 1 ? ceintlResult.firstPage[0] : tagResult.total === 1 ? tagResult.firstPage[0] : null; - const extensionToFetchTranslationsFrom = extensionToInstall || tagResult.total > 0 ? tagResult.firstPage[0] : null; + const extensionToInstall = tagResult.total === 1 ? tagResult.firstPage[0] : tagResult.firstPage.filter(e => e.publisher === 'MS-CEINTL' && e.name.indexOf('vscode-language-pack') === 0)[0]; + const extensionToFetchTranslationsFrom = extensionToInstall || tagResult.firstPage[0]; - if (!extensionToFetchTranslationsFrom || !extensionToFetchTranslationsFrom.assets.manifest) { + if (!extensionToFetchTranslationsFrom.assets.manifest) { return; } - TPromise.join([this.galleryService.getManifest(extensionToFetchTranslationsFrom), this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, locale)]) + TPromise.join([this.galleryService.getManifest(extensionToFetchTranslationsFrom, CancellationToken.None), this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, locale)]) .then(([manifest, translation]) => { const loc = manifest && manifest.contributes && manifest.contributes.localizations && manifest.contributes.localizations.filter(x => x.languageId.toLowerCase() === locale)[0]; + const languageName = loc ? (loc.languageName || locale) : locale; const languageDisplayName = loc ? (loc.localizedLanguageName || loc.languageName || locale) : locale; - const translations = { - ...minimumTranslatedStrings, - ...(translation && translation.contents ? translation.contents['vs/platform/node/minimalTranslations'] : {}) - }; + const translationsFromPack = translation && translation.contents ? translation.contents['vs/platform/node/minimalTranslations'] : {}; + const promptMessageKey = extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions'; + const useEnglish = !translationsFromPack[promptMessageKey]; + + const translations = {}; + Object.keys(minimumTranslatedStrings).forEach(key => { + if (!translationsFromPack[key] || useEnglish) { + translations[key] = minimumTranslatedStrings[key].replace('{0}', languageName); + } else { + translations[key] = `${translationsFromPack[key].replace('{0}', languageDisplayName)} (${minimumTranslatedStrings[key].replace('{0}', languageName)})`; + } + }); + const logUserReaction = (userReaction: string) => { /* __GDPR__ "languagePackSuggestion:popup" : { @@ -195,8 +181,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } }; - const promptMessage = translations[extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions'] - .replace('{0}', languageDisplayName); + const promptMessage = translations[promptMessageKey]; this.notificationService.prompt( Severity.Info, @@ -226,19 +211,6 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } - private getLanguagePackExtension(language: string): TPromise { - return this.localizationService.getLanguageIds(LanguageType.Core) - .then(coreLanguages => { - if (coreLanguages.some(c => c.toLowerCase() === language)) { - const extensionIdPrefix = language === 'zh-cn' ? 'zh-hans' : language === 'zh-tw' ? 'zh-hant' : language; - const extensionId = `MS-CEINTL.vscode-language-pack-${extensionIdPrefix}`; - return this.galleryService.query({ names: [extensionId], pageSize: 1 }) - .then(result => result.total === 1 ? result.firstPage[0] : null); - } - return null; - }); - } - private isLanguageInstalled(language: string): TPromise { return this.extensionManagementService.getInstalled(LocalExtensionType.User) .then(installed => installed.some(i => i.manifest && i.manifest.contributes && i.manifest.contributes.localizations && i.manifest.contributes.localizations.length && i.manifest.contributes.localizations.some(l => l.languageId.toLowerCase() === language))); diff --git a/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts b/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts index 77fa7330109..7f1c8f27444 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizationsActions.ts @@ -6,15 +6,14 @@ import { localize } from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; import { IEditor } from 'vs/workbench/common/editor'; import { join } from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getPathLabel } from 'vs/base/common/labels'; import { language } from 'vs/base/common/platform'; +import { ILabelService } from 'vs/platform/label/common/label'; export class ConfigureLocaleAction extends Action { public static readonly ID = 'workbench.action.configureLocale'; @@ -22,18 +21,18 @@ export class ConfigureLocaleAction extends Action { private static DEFAULT_CONTENT: string = [ '{', - `\t// ${localize('displayLanguage', 'Defines VSCode\'s display language.')}`, + `\t// ${localize('displayLanguage', 'Defines VS Code\'s display language.')}`, `\t// ${localize('doc', 'See {0} for a list of supported languages.', 'https://go.microsoft.com/fwlink/?LinkId=761051')}`, `\t`, - `\t"locale":"${language}" // ${localize('restart', 'Changes will not take effect until VSCode has been restarted.')}`, + `\t"locale":"${language}" // ${localize('restart', 'Changes will not take effect until VS Code has been restarted.')}`, '}' ].join('\n'); constructor(id: string, label: string, @IFileService private fileService: IFileService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, @IEnvironmentService private environmentService: IEnvironmentService, - @IEditorService private editorService: IEditorService + @IEditorService private editorService: IEditorService, + @ILabelService private labelService: ILabelService ) { super(id, label); } @@ -47,13 +46,10 @@ export class ConfigureLocaleAction extends Action { return undefined; } return this.editorService.openEditor({ - resource: stat.resource, - options: { - forceOpen: true - } + resource: stat.resource }); }, (error) => { - throw new Error(localize('fail.createSettings', "Unable to create '{0}' ({1}).", getPathLabel(file, this.contextService), error)); + throw new Error(localize('fail.createSettings', "Unable to create '{0}' ({1}).", this.labelService.getUriLabel(file, true), error)); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts index 78e0289be53..b0df3ba7f16 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts @@ -12,35 +12,29 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { Disposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; import * as Constants from 'vs/workbench/parts/logs/common/logConstants'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ShowLogsAction, OpenLogsFolderAction, SetLogLevelAction, OpenLogFileAction } from 'vs/workbench/parts/logs/electron-browser/logsActions'; - +import { OpenLogsFolderAction, SetLogLevelAction } from 'vs/workbench/parts/logs/electron-browser/logsActions'; class LogOutputChannels extends Disposable implements IWorkbenchContribution { constructor( - @IWindowService private windowService: IWindowService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IInstantiationService instantiationService: IInstantiationService + @IWindowService windowService: IWindowService, + @IEnvironmentService environmentService: IEnvironmentService, ) { super(); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); - outputChannelRegistry.registerChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Log (Main)"), URI.file(join(this.environmentService.logsPath, `main.log`))); - outputChannelRegistry.registerChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Log (Shared)"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); - outputChannelRegistry.registerChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Log (Window)"), URI.file(join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`))); - outputChannelRegistry.registerChannel(Constants.extHostLogChannelId, nls.localize('extensionsLog', "Log (Extension Host)"), URI.file(join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`))); + outputChannelRegistry.registerChannel({ id: Constants.mainLogChannelId, label: nls.localize('mainLog', "Main"), file: URI.file(join(environmentService.logsPath, `main.log`)), log: true }); + outputChannelRegistry.registerChannel({ id: Constants.sharedLogChannelId, label: nls.localize('sharedLog', "Shared"), file: URI.file(join(environmentService.logsPath, `sharedprocess.log`)), log: true }); + outputChannelRegistry.registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Window"), file: URI.file(join(environmentService.logsPath, `renderer${windowService.getCurrentWindowId()}.log`)), log: true }); const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Log Folder', devCategory); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.LABEL), 'Developer: Set Log Level', devCategory); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowLogsAction, ShowLogsAction.ID, ShowLogsAction.LABEL), 'Developer: Show Logs...', devCategory); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogFileAction, OpenLogFileAction.ID, OpenLogFileAction.LABEL), 'Developer: Open Log File...', devCategory); } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Eventually); \ No newline at end of file +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Starting); \ No newline at end of file diff --git a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts index 4ebfb3a4e56..bfaf241b326 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts @@ -7,15 +7,10 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import * as paths from 'vs/base/common/paths'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { ILogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; -import { IOutputService, COMMAND_OPEN_LOG_VIEWER } from 'vs/workbench/parts/output/common/output'; -import * as Constants from 'vs/workbench/parts/logs/common/logConstants'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import URI from 'vs/base/common/uri'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class OpenLogsFolderAction extends Action { @@ -34,77 +29,13 @@ export class OpenLogsFolderAction extends Action { } } -export class ShowLogsAction extends Action { - - static ID = 'workbench.action.showLogs'; - static LABEL = nls.localize('showLogs', "Show Logs..."); - - constructor(id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, - @IOutputService private outputService: IOutputService, - @IWorkspaceContextService private contextService: IWorkspaceContextService - ) { - super(id, label); - } - - run(): TPromise { - const entries: IPickOpenEntry[] = [ - { id: Constants.rendererLogChannelId, label: this.contextService.getWorkspace().name ? nls.localize('rendererProcess', "Window ({0})", this.contextService.getWorkspace().name) : nls.localize('emptyWindow', "Window") }, - { id: Constants.extHostLogChannelId, label: nls.localize('extensionHost', "Extension Host") }, - { id: Constants.sharedLogChannelId, label: nls.localize('sharedProcess', "Shared") }, - { id: Constants.mainLogChannelId, label: nls.localize('mainProcess', "Main") } - ]; - - return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectProcess', "Select Log for Process") }) - .then(entry => { - if (entry) { - return this.outputService.showChannel(entry.id); - } - return null; - }); - } -} - -export class OpenLogFileAction extends Action { - - static ID = 'workbench.action.openLogFile'; - static LABEL = nls.localize('openLogFile', "Open Log File..."); - - constructor(id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ICommandService private commandService: ICommandService, - @IWindowService private windowService: IWindowService, - @IWorkspaceContextService private contextService: IWorkspaceContextService - ) { - super(id, label); - } - - run(): TPromise { - const entries: IPickOpenEntry[] = [ - { id: URI.file(paths.join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`)).fsPath, label: this.contextService.getWorkspace().name ? nls.localize('rendererProcess', "Window ({0})", this.contextService.getWorkspace().name) : nls.localize('emptyWindow', "Window") }, - { id: URI.file(paths.join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`)).fsPath, label: nls.localize('extensionHost', "Extension Host") }, - { id: URI.file(paths.join(this.environmentService.logsPath, `sharedprocess.log`)).fsPath, label: nls.localize('sharedProcess', "Shared") }, - { id: URI.file(paths.join(this.environmentService.logsPath, `main.log`)).fsPath, label: nls.localize('mainProcess', "Main") } - ]; - - return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectProcess', "Select Log for Process") }) - .then(entry => { - if (entry) { - return this.commandService.executeCommand(COMMAND_OPEN_LOG_VIEWER, URI.file(entry.id)); - } - return null; - }); - } -} - export class SetLogLevelAction extends Action { static ID = 'workbench.action.setLogLevel'; static LABEL = nls.localize('setLogLevel', "Set Log Level..."); constructor(id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @ILogService private logService: ILogService ) { super(id, label); @@ -122,7 +53,7 @@ export class SetLogLevelAction extends Action { { label: nls.localize('off', "Off"), level: LogLevel.Off, description: this.getDescription(LogLevel.Off, current) }, ]; - return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectLogLevel', "Select log level"), autoFocus: { autoFocusIndex: this.logService.getLevel() } }).then(entry => { + return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectLogLevel', "Select log level"), activeItem: entries[this.logService.getLevel()] }).then(entry => { if (entry) { this.logService.setLevel(entry.level); } @@ -141,4 +72,4 @@ export class SetLogLevelAction extends Action { } return void 0; } -} \ No newline at end of file +} 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 789c79ff77b..8ed5e8f4b35 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.contribution.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Marker, RelatedInformation } from 'vs/workbench/parts/markers/electron-browser/markersModel'; @@ -27,7 +27,7 @@ import './markersFileDecorations'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_OPEN_SIDE_ACTION_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.MarkerFocusContextKey), primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { @@ -41,7 +41,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_SHOW_PANEL_ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, handler: (accessor, args: any) => { @@ -212,3 +212,12 @@ function registerAction(desc: IActionDescriptor) { }); } } + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: ToggleMarkersPanelAction.ID, + title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") + }, + order: 4 +}); diff --git a/src/vs/workbench/parts/markers/electron-browser/markers.ts b/src/vs/workbench/parts/markers/electron-browser/markers.ts index 2c104743afd..12ea873cd7c 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markers.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markers.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkersModel, FilterOptions } from './markersModel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { localize } from 'vs/nls'; import Constants from './constants'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -49,10 +49,11 @@ export class MarkersWorkbenchService extends Disposable implements IMarkersWorkb @IMarkerService private markerService: IMarkerService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IActivityService private activityService: IActivityService + @IActivityService private activityService: IActivityService, + @IInstantiationService instantiationService: IInstantiationService ) { super(); - this.markersModel = this._register(new MarkersModel(this.readMarkers())); + this.markersModel = this._register(instantiationService.createInstance(MarkersModel, this.readMarkers())); this._register(markerService.onMarkerChanged(resources => this.onMarkerChanged(resources))); this._register(configurationService.onDidChangeConfiguration(e => { if (this.useFilesExclude && e.affectsConfiguration('files.exclude')) { diff --git a/src/vs/workbench/parts/markers/electron-browser/markersFileDecorations.ts b/src/vs/workbench/parts/markers/electron-browser/markersFileDecorations.ts index 5f5b3ba56df..08d9f89fec3 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersFileDecorations.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersFileDecorations.ts @@ -9,7 +9,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as import { IMarkerService, IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IDecorationsService, IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -49,7 +49,7 @@ class MarkersDecorationsProvider implements IDecorationsProvider { weight: 100 * first.severity, bubble: true, tooltip: markers.length === 1 ? localize('tooltip.1', "1 problem in this file") : localize('tooltip.N', "{0} problems in this file", markers.length), - letter: markers.length < 10 ? markers.length.toString() : '+9', + letter: markers.length < 10 ? markers.length.toString() : '9+', color: first.severity === MarkerSeverity.Error ? listErrorForeground : listWarningForeground, }; } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts index 2345616c2bb..8a8b3499a33 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersModel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersModel.ts @@ -5,9 +5,9 @@ 'use strict'; import * as paths from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range, IRange } from 'vs/editor/common/core/range'; -import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; +import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers'; import { IFilter, IMatch, or, matchesContiguousSubString, matchesPrefix, matchesFuzzy } from 'vs/base/common/filters'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import { Schemas } from 'vs/base/common/network'; @@ -15,6 +15,10 @@ import { groupBy, isFalsyOrEmpty, flatten } from 'vs/base/common/arrays'; import { values } from 'vs/base/common/map'; import * as glob from 'vs/base/common/glob'; import * as strings from 'vs/base/common/strings'; +import { CodeAction } from 'vs/editor/common/modes'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; +import { IModelService } from 'vs/editor/common/services/modelService'; function compareUris(a: URI, b: URI) { if (a.toString() < b.toString()) { @@ -34,6 +38,7 @@ export class ResourceMarkers extends NodeWithId { private _name: string = null; private _path: string = null; + private _allFixesPromise: Promise; markers: Marker[] = []; isExcluded: boolean = false; @@ -42,7 +47,8 @@ export class ResourceMarkers extends NodeWithId { uriMatches: IMatch[] = []; constructor( - readonly uri: URI + readonly uri: URI, + private modelService: IModelService ) { super(uri.toString()); } @@ -61,6 +67,39 @@ export class ResourceMarkers extends NodeWithId { return this._name; } + public getFixes(marker: Marker): Promise { + return this._getFixes(new Range(marker.range.startLineNumber, marker.range.startColumn, marker.range.endLineNumber, marker.range.endColumn)); + } + + public async hasFixes(marker: Marker): Promise { + if (!this.modelService.getModel(this.uri)) { + // Return early, If the model is not yet created + return false; + } + if (!this._allFixesPromise) { + this._allFixesPromise = this._getFixes(); + } + const allFixes = await this._allFixesPromise; + if (allFixes.length) { + const markerKey = IMarkerData.makeKey(marker.raw); + for (const fix of allFixes) { + if (fix.diagnostics && fix.diagnostics.some(d => IMarkerData.makeKey(d) === markerKey)) { + return true; + } + } + } + return false; + } + + private async _getFixes(range?: Range): Promise { + const model = this.modelService.getModel(this.uri); + if (model) { + const codeActions = await getCodeActions(model, range ? range : model.getFullModelRange(), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }); + return codeActions; + } + return []; + } + static compare(a: ResourceMarkers, b: ResourceMarkers): number { let [firstMarkerOfA] = a.markers; let [firstMarkerOfB] = b.markers; @@ -85,6 +124,7 @@ export class Marker extends NodeWithId { constructor( id: string, readonly raw: IMarker, + readonly resourceMarkers: ResourceMarkers ) { super(id); } @@ -185,7 +225,10 @@ export class MarkersModel { private _markersByResource: Map; private _filterOptions: FilterOptions; - constructor(markers: IMarker[] = []) { + constructor( + markers: IMarker[] = [], + @IModelService private modelService: IModelService + ) { this._markersByResource = new Map(); this._filterOptions = new FilterOptions(); @@ -270,11 +313,11 @@ export class MarkersModel { private createResource(uri: URI, rawMarkers: IMarker[]): ResourceMarkers { const markers: Marker[] = []; - const resource = new ResourceMarkers(uri); + const resource = new ResourceMarkers(uri, this.modelService); this.updateResource(resource); rawMarkers.forEach((rawMarker, index) => { - const marker = new Marker(uri.toString() + index, rawMarker); + const marker = new Marker(uri.toString() + index, rawMarker, resource); if (rawMarker.relatedInformation) { const groupedByResource = groupBy(rawMarker.relatedInformation, MarkersModel._compareMarkersByUri); groupedByResource.sort((a, b) => compareUris(a[0].resource, b[0].resource)); diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts index 22654994ff3..da84a55a5ba 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanel.ts @@ -5,8 +5,7 @@ import 'vs/css!./media/markers'; -import * as errors from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Delayer } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; @@ -19,7 +18,7 @@ import { Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/parts/ import { Controller } from 'vs/workbench/parts/markers/electron-browser/markersTreeController'; import * as Viewer from 'vs/workbench/parts/markers/electron-browser/markersTreeViewer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CollapseAllAction, MarkersFilterActionItem, MarkersFilterAction } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; +import { CollapseAllAction, MarkersFilterActionItem, MarkersFilterAction, QuickFixAction, QuickFixActionItem, IMarkersFilterActionChangeEvent } 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'; @@ -32,6 +31,8 @@ import { SimpleFileResourceDragAndDrop } from 'vs/workbench/browser/dnd'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Scope } from 'vs/workbench/common/memento'; import { localize } from 'vs/nls'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; export class MarkersPanel extends Panel { @@ -46,10 +47,12 @@ export class MarkersPanel extends Panel { private actions: IAction[]; private collapseAllAction: IAction; + private filterAction: MarkersFilterAction; private filterInputActionItem: MarkersFilterActionItem; private treeContainer: HTMLElement; private messageBoxContainer: HTMLElement; + private ariaLabelElement: HTMLElement; private panelSettings: any; private currentResourceGotAddedToMarkersData: boolean = false; @@ -67,18 +70,19 @@ export class MarkersPanel extends Panel { this.delayedRefresh = new Delayer(500); this.autoExpanded = new Set(); this.panelSettings = this.getMemento(storageService, Scope.WORKSPACE); + this.setCurrentActiveEditor(); } public create(parent: HTMLElement): TPromise { super.create(parent); - this.rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations); - this.toUnbind.push(this.rangeHighlightDecorations); + this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations)); dom.addClass(parent, 'markers-panel'); - let container = dom.append(parent, dom.$('.markers-panel-container')); + const container = dom.append(parent, dom.$('.markers-panel-container')); + this.createArialLabelElement(container); this.createMessageBox(container); this.createTree(container); this.createActions(); @@ -96,7 +100,9 @@ export class MarkersPanel extends Panel { public layout(dimension: dom.Dimension): void { this.treeContainer.style.height = `${dimension.height}px`; this.tree.layout(dimension.height, dimension.width); - this.filterInputActionItem.toggleLayout(dimension.width < 1200); + if (this.filterInputActionItem) { + this.filterInputActionItem.toggleLayout(dimension.width < 1200); + } } public focus(): void { @@ -149,13 +155,13 @@ export class MarkersPanel extends Panel { pinned, revealIfVisible: true }, - }, sideByside ? SIDE_GROUP : ACTIVE_GROUP).done(editor => { + }, sideByside ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { if (editor && preserveFocus) { this.rangeHighlightDecorations.highlightRange({ resource, range: selection }, editor.getControl()); } else { this.rangeHighlightDecorations.removeHighlightRange(); } - }, errors.onUnexpectedError); + }); return true; } else { this.rangeHighlightDecorations.removeHighlightRange(); @@ -179,17 +185,23 @@ export class MarkersPanel extends Panel { private updateFilter() { this.autoExpanded = new Set(); - this.markersWorkbenchService.filter({ filterText: this.filterInputActionItem.getFilterText(), useFilesExclude: this.filterInputActionItem.useFilesExclude }); + this.markersWorkbenchService.filter({ filterText: this.filterAction.filterText, useFilesExclude: this.filterAction.useFilesExclude }); } private createMessageBox(parent: HTMLElement): void { this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container')); + this.messageBoxContainer.setAttribute('aria-labelledby', 'markers-panel-arialabel'); + } + + private createArialLabelElement(parent: HTMLElement): void { + this.ariaLabelElement = dom.append(parent, dom.$('')); + this.ariaLabelElement.setAttribute('id', 'markers-panel-arialabel'); + this.ariaLabelElement.setAttribute('aria-live', 'polite'); } private createTree(parent: HTMLElement): void { - this.treeContainer = dom.append(parent, dom.$('.tree-container')); - dom.addClass(this.treeContainer, 'show-file-icons'); - const renderer = this.instantiationService.createInstance(Viewer.Renderer); + this.treeContainer = dom.append(parent, dom.$('.tree-container.show-file-icons')); + const renderer = this.instantiationService.createInstance(Viewer.Renderer, (action) => this.getActionItem(action)); const dnd = this.instantiationService.createInstance(SimpleFileResourceDragAndDrop, obj => obj instanceof ResourceMarkers ? obj.uri : void 0); const controller = this.instantiationService.createInstance(Controller); this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { @@ -197,7 +209,7 @@ export class MarkersPanel extends Panel { filter: new Viewer.DataFilter(), renderer, controller, - accessibilityProvider: new Viewer.MarkersTreeAccessibilityProvider(), + accessibilityProvider: this.instantiationService.createInstance(Viewer.MarkersTreeAccessibilityProvider), dnd }, { twistiePixels: 20, @@ -224,17 +236,20 @@ export class MarkersPanel extends Panel { private createActions(): void { this.collapseAllAction = this.instantiationService.createInstance(CollapseAllAction, this.tree, true); - const filterAction = this.instantiationService.createInstance(MarkersFilterAction); - this.filterInputActionItem = this.instantiationService.createInstance(MarkersFilterActionItem, { filterText: this.panelSettings['filter'] || '', filterHistory: this.panelSettings['filterHistory'] || [], useFilesExclude: !!this.panelSettings['useFilesExclude'] }, filterAction); - this.actions = [filterAction, this.collapseAllAction]; + this.filterAction = this.instantiationService.createInstance(MarkersFilterAction, { filterText: this.panelSettings['filter'] || '', filterHistory: this.panelSettings['filterHistory'] || [], useFilesExclude: !!this.panelSettings['useFilesExclude'] }); + this.actions = [this.filterAction, this.collapseAllAction]; } private createListeners(): void { - this.toUnbind.push(this.markersWorkbenchService.onDidChange(resources => this.onDidChange(resources))); - this.toUnbind.push(this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this)); - this.toUnbind.push(this.tree.onDidChangeSelection(() => this.onSelected())); - this.toUnbind.push(this.filterInputActionItem.onDidChange(() => this.updateFilter())); - this.actions.forEach(a => this.toUnbind.push(a)); + this._register(this.markersWorkbenchService.onDidChange(resources => this.onDidChange(resources))); + this._register(this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this)); + this._register(this.tree.onDidChangeSelection(() => this.onSelected())); + this._register(this.filterAction.onDidChange((event: IMarkersFilterActionChangeEvent) => { + if (event.filterText || event.useFilesExclude) { + this.updateFilter(); + } + })); + this.actions.forEach(a => this._register(a)); } private onDidChange(resources: URI[]) { @@ -262,9 +277,13 @@ export class MarkersPanel extends Panel { } private onActiveEditorChanged(): void { + this.setCurrentActiveEditor(); + this.autoReveal(); + } + + private setCurrentActiveEditor(): void { const activeEditor = this.editorService.activeEditor; this.currentActiveResource = activeEditor ? activeEditor.getResource() : void 0; - this.autoReveal(); } private onSelected(): void { @@ -292,11 +311,20 @@ export class MarkersPanel extends Panel { } private renderMessage(): void { - const markersModel = this.markersWorkbenchService.markersModel; - const hasFilteredResources = markersModel.hasFilteredResources(); dom.clearNode(this.messageBoxContainer); - dom.toggleClass(this.messageBoxContainer, 'hidden', hasFilteredResources); - if (!hasFilteredResources) { + const markersModel = this.markersWorkbenchService.markersModel; + if (markersModel.hasFilteredResources()) { + this.messageBoxContainer.style.display = 'none'; + const { total, filtered } = markersModel.stats(); + if (filtered === total) { + this.ariaLabelElement.setAttribute('aria-label', localize('No problems filtered', "Showing {0} problems", total)); + } else { + this.ariaLabelElement.setAttribute('aria-label', localize('problems filtered', "Showing {0} of {1} problems", filtered, total)); + } + this.messageBoxContainer.removeAttribute('tabIndex'); + } else { + this.messageBoxContainer.style.display = 'block'; + this.messageBoxContainer.setAttribute('tabIndex', '0'); if (markersModel.hasResources()) { if (markersModel.filterOptions.filter) { this.renderFilteredByFilterMessage(this.messageBoxContainer); @@ -315,7 +343,14 @@ export class MarkersPanel extends Panel { const link = dom.append(container, dom.$('a.messageAction')); link.textContent = localize('disableFilesExclude', "Disable Files Exclude Filter."); link.setAttribute('tabIndex', '0'); - dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.useFilesExclude = false); + dom.addStandardDisposableListener(link, dom.EventType.CLICK, () => this.filterAction.useFilesExclude = false); + dom.addStandardDisposableListener(link, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { + this.filterAction.useFilesExclude = false; + e.stopPropagation(); + } + }); + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILE_EXCLUSIONS_FILTER); } private renderFilteredByFilterMessage(container: HTMLElement) { @@ -324,18 +359,26 @@ export class MarkersPanel extends Panel { const link = dom.append(container, dom.$('a.messageAction')); link.textContent = localize('clearFilter', "Clear Filter."); link.setAttribute('tabIndex', '0'); - dom.addDisposableListener(link, dom.EventType.CLICK, () => this.filterInputActionItem.clear()); + dom.addStandardDisposableListener(link, dom.EventType.CLICK, () => this.filterAction.filterText = ''); + dom.addStandardDisposableListener(link, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { + this.filterAction.filterText = ''; + e.stopPropagation(); + } + }); + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS); } private renderNoProblemsMessage(container: HTMLElement) { const span = dom.append(container, dom.$('span')); span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT); } private autoExpand(): void { this.markersWorkbenchService.markersModel.forEachFilteredResource(resource => { if (!this.autoExpanded.has(resource.uri.toString())) { - this.tree.expand(resource).done(null, errors.onUnexpectedError); + this.tree.expand(resource); this.autoExpanded.add(resource.uri.toString()); } }); @@ -414,16 +457,20 @@ export class MarkersPanel extends Panel { public getActionItem(action: IAction): IActionItem { if (action.id === MarkersFilterAction.ID) { + this.filterInputActionItem = this.instantiationService.createInstance(MarkersFilterActionItem, this.filterAction); return this.filterInputActionItem; } + if (action.id === QuickFixAction.ID) { + return this.instantiationService.createInstance(QuickFixActionItem, action); + } return super.getActionItem(action); } public shutdown(): void { // store memento - this.panelSettings['filter'] = this.filterInputActionItem.getFilterText(); - this.panelSettings['filterHistory'] = this.filterInputActionItem.getFilterHistory(); - this.panelSettings['useFilesExclude'] = this.filterInputActionItem.useFilesExclude; + this.panelSettings['filter'] = this.filterAction.filterText; + this.panelSettings['filterHistory'] = this.filterAction.filterHistory; + this.panelSettings['useFilesExclude'] = this.filterAction.useFilesExclude; super.shutdown(); } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts index eecafd66e38..744c78de4e2 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersPanelActions.ts @@ -6,11 +6,11 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, IActionChangeEvent } from 'vs/base/common/actions'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; import Messages from 'vs/workbench/parts/markers/electron-browser/messages'; import Constants from 'vs/workbench/parts/markers/electron-browser/constants'; @@ -22,14 +22,20 @@ import * as Tree from 'vs/base/parts/tree/browser/tree'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachStylerCallback, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; import { IMarkersWorkbenchService } from 'vs/workbench/parts/markers/electron-browser/markers'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { BaseActionItem, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { badgeBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/input'; +import { ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import { Marker } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { isEqual } from 'vs/base/common/resources'; export class ToggleMarkersPanelAction extends TogglePanelAction { @@ -68,39 +74,63 @@ export class CollapseAllAction extends TreeCollapseAction { } } -export class MarkersFilterAction extends Action { - - public static readonly ID: string = 'workbench.actions.problems.filter'; - - constructor() { - super(MarkersFilterAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_FILTER, 'markers-panel-action-filter', true); - } - +export interface IMarkersFilterActionChangeEvent extends IActionChangeEvent { + filterText?: boolean; + useFilesExclude?: boolean; } -export interface IMarkersFilterActionItemOptions { +export interface IMarkersFilterActionOptions { filterText: string; filterHistory: string[]; useFilesExclude: boolean; } +export class MarkersFilterAction extends Action { + + public static readonly ID: string = 'workbench.actions.problems.filter'; + + constructor(options: IMarkersFilterActionOptions) { + super(MarkersFilterAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_FILTER, 'markers-panel-action-filter', true); + this._filterText = options.filterText; + this._useFilesExclude = options.useFilesExclude; + this.filterHistory = options.filterHistory; + } + + private _filterText: string; + get filterText(): string { + return this._filterText; + } + set filterText(filterText: string) { + if (this._filterText !== filterText) { + this._filterText = filterText; + this._onDidChange.fire({ filterText: true }); + } + } + + filterHistory: string[]; + + private _useFilesExclude: boolean; + get useFilesExclude(): boolean { + return this._useFilesExclude; + } + set useFilesExclude(filesExclude: boolean) { + if (this._useFilesExclude !== filesExclude) { + this._useFilesExclude = filesExclude; + this._onDidChange.fire({ useFilesExclude: true }); + } + } +} + export class MarkersFilterActionItem extends BaseActionItem { - private _toDispose: IDisposable[] = []; - - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - private delayedFilterUpdate: Delayer; private container: HTMLElement; private filterInputBox: HistoryInputBox; private controlsContainer: HTMLInputElement; private filterBadge: HTMLInputElement; - private filesExcludeFilter: Checkbox; constructor( - private itemOptions: IMarkersFilterActionItemOptions, - action: IAction, + readonly action: MarkersFilterAction, @IInstantiationService private instantiationService: IInstantiationService, @IContextViewService private contextViewService: IContextViewService, @IThemeService private themeService: IThemeService, @@ -109,45 +139,23 @@ export class MarkersFilterActionItem extends BaseActionItem { ) { super(null, action); this.delayedFilterUpdate = new Delayer(500); + this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); } render(container: HTMLElement): void { this.container = container; - DOM.addClass(this.container, 'markers-panel-action-filter'); - this.createInput(this.container); - this.createControls(this.container); + DOM.addClass(this.container, 'markers-panel-action-filter-container'); + + const filterContainer = DOM.append(this.container, DOM.$('.markers-panel-action-filter')); + this.createInput(filterContainer); + this.createControls(filterContainer); + this.adjustInputBox(); } - clear(): void { - this.filterInputBox.value = ''; - } - - getFilterText(): string { - return this.filterInputBox ? this.filterInputBox.value : this.itemOptions.filterText; - } - - getFilterHistory(): string[] { - return this.filterInputBox.getHistory(); - } - - get useFilesExclude(): boolean { - return this.filesExcludeFilter ? this.filesExcludeFilter.checked : this.itemOptions.useFilesExclude; - } - - set useFilesExclude(useFilesExclude: boolean) { - if (this.filesExcludeFilter) { - if (this.filesExcludeFilter.checked !== useFilesExclude) { - this.filesExcludeFilter.checked = useFilesExclude; - this._onDidChange.fire(); - } - } - } - toggleLayout(small: boolean) { if (this.container) { DOM.toggleClass(this.container, 'small', small); - DOM.toggleClass(this.filterBadge, 'small', small); } } @@ -155,11 +163,17 @@ export class MarkersFilterActionItem extends BaseActionItem { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.itemOptions.filterHistory + history: this.action.filterHistory })); + this.filterInputBox.inputElement.setAttribute('aria-labelledby', 'markers-panel-arialabel'); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.itemOptions.filterText; - this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange()))); + this.filterInputBox.value = this.action.filterText; + this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox)))); + this._register(this.action.onDidChange((event: IMarkersFilterActionChangeEvent) => { + if (event.filterText) { + this.filterInputBox.value = this.action.filterText; + } + })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, 'keydown', (keyboardEvent) => this.onInputKeyDown(keyboardEvent, this.filterInputBox))); this._register(DOM.addStandardDisposableListener(container, 'keydown', this.handleKeyboardEvent)); this._register(DOM.addStandardDisposableListener(container, 'keyup', this.handleKeyboardEvent)); @@ -188,25 +202,29 @@ export class MarkersFilterActionItem extends BaseActionItem { } private createFilesExcludeCheckbox(container: HTMLElement): void { - this.filesExcludeFilter = new Checkbox({ + const filesExcludeFilter = this._register(new Checkbox({ actionClassName: 'markers-panel-filter-filesExclude', - title: this.itemOptions.useFilesExclude ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE, - isChecked: this.itemOptions.useFilesExclude, - onChange: () => { - this.filesExcludeFilter.domNode.title = this.filesExcludeFilter.checked ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE; - this._onDidChange.fire(); + title: this.action.useFilesExclude ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE, + isChecked: this.action.useFilesExclude + })); + this._register(filesExcludeFilter.onChange(() => { + filesExcludeFilter.domNode.title = filesExcludeFilter.checked ? Messages.MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE : Messages.MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE; + this.action.useFilesExclude = filesExcludeFilter.checked; + })); + this._register(this.action.onDidChange((event: IMarkersFilterActionChangeEvent) => { + if (event.useFilesExclude) { + filesExcludeFilter.checked = this.action.useFilesExclude; } - }); - this._register(attachCheckboxStyler(this.filesExcludeFilter, this.themeService)); - container.appendChild(this.filesExcludeFilter.domNode); + })); + + this._register(attachCheckboxStyler(filesExcludeFilter, this.themeService)); + container.appendChild(filesExcludeFilter.domNode); } - private onDidInputChange() { - const filterText = this.filterInputBox.value; - if (filterText) { - this.filterInputBox.addToHistory(filterText); - } - this._onDidChange.fire(); + private onDidInputChange(inputbox: HistoryInputBox) { + inputbox.addToHistory(); + this.action.filterText = inputbox.value; + this.action.filterHistory = inputbox.getHistory(); this.reportFilteringUsed(); } @@ -261,9 +279,89 @@ export class MarkersFilterActionItem extends BaseActionItem { */ this.telemetryService.publicLog('problems.filter', data); } +} + +export class QuickFixAction extends Action { + + public static readonly ID: string = 'workbench.actions.problems.quickfix'; + + private updated: boolean = false; + private disposables: IDisposable[] = []; + + constructor( + readonly marker: Marker, + @IBulkEditService private bulkEditService: IBulkEditService, + @ICommandService private commandService: ICommandService, + @IEditorService private editorService: IEditorService, + @IModelService modelService: IModelService + ) { + super(QuickFixAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX, 'markers-panel-action-quickfix', false); + if (modelService.getModel(this.marker.resourceMarkers.uri)) { + this.update(); + } else { + modelService.onModelAdded(model => { + if (isEqual(model.uri, marker.resource)) { + this.update(); + } + }, this, this.disposables); + } - private _register(t: T): T { - this._toDispose.push(t); - return t; } + + private update(): void { + if (!this.updated) { + this.marker.resourceMarkers.hasFixes(this.marker).then(hasFixes => this.enabled = hasFixes); + this.updated = true; + } + } + + async getQuickFixActions(): Promise { + const codeActions = await this.marker.resourceMarkers.getFixes(this.marker); + return codeActions.map(codeAction => new Action( + codeAction.command ? codeAction.command.id : codeAction.title, + codeAction.title, + void 0, + true, + () => { + return this.openFileAtMarker(this.marker) + .then(() => applyCodeAction(codeAction, this.bulkEditService, this.commandService)); + })); + } + + public openFileAtMarker(element: Marker): TPromise { + const { resource, selection } = { resource: element.resource, selection: element.range }; + return this.editorService.openEditor({ + resource, + options: { + selection, + preserveFocus: true, + pinned: false, + revealIfVisible: true + }, + }, ACTIVE_GROUP).then(() => null); + } + + dispose(): void { + dispose(this.disposables); + super.dispose(); + } +} + +export class QuickFixActionItem extends ActionItem { + + constructor(action: QuickFixAction, + @IContextMenuService private contextMenuService: IContextMenuService + ) { + super(null, action, { icon: true, label: false }); + } + + public onClick(event: DOM.EventLike): void { + DOM.EventHelper.stop(event, true); + const elementPosition = DOM.getDomNodePagePosition(this.element); + this.contextMenuService.showContextMenu({ + getAnchor: () => ({ x: elementPosition.left + 10, y: elementPosition.top + elementPosition.height }), + getActions: () => TPromise.wrap((this.getAction()).getQuickFixActions()), + }); + } + } \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts index 930f8c9ee45..e936b16e3f1 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeController.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as mouse from 'vs/base/browser/mouseEvent'; import * as tree from 'vs/base/parts/tree/browser/tree'; -import { MarkersModel } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { MarkersModel, Marker } from 'vs/workbench/parts/markers/electron-browser/markersModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; @@ -15,6 +15,8 @@ import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { QuickFixAction } from 'vs/workbench/parts/markers/electron-browser/markersPanelActions'; export class Controller extends WorkbenchTreeController { @@ -22,7 +24,8 @@ export class Controller extends WorkbenchTreeController { @IContextMenuService private contextMenuService: IContextMenuService, @IMenuService private menuService: IMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService private instantiationService: IInstantiationService ) { super({}, configurationService); } @@ -45,17 +48,11 @@ export class Controller extends WorkbenchTreeController { public onContextMenu(tree: WorkbenchTree, element: any, event: tree.ContextMenuEvent): boolean { tree.setFocus(element, { preventOpenOnFocus: true }); - const actions = this._getMenuActions(tree); - if (!actions.length) { - return true; - } const anchor = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => { - return TPromise.as(actions); - }, + getActions: () => TPromise.wrap(this._getMenuActions(tree, element)), getActionItem: (action) => { const keybinding = this._keybindingService.lookupKeybinding(action.id); @@ -75,8 +72,18 @@ export class Controller extends WorkbenchTreeController { return true; } - private _getMenuActions(tree: WorkbenchTree): IAction[] { + private async _getMenuActions(tree: WorkbenchTree, element: any): Promise { const result: IAction[] = []; + + if (element instanceof Marker) { + const quickFixAction = this.instantiationService.createInstance(QuickFixAction, element); + const quickFixActions = await quickFixAction.getQuickFixActions(); + if (quickFixActions.length) { + result.push(...quickFixActions); + result.push(new Separator()); + } + } + const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, tree.contextKeyService); const groups = menu.getActions(); menu.dispose(); @@ -86,6 +93,7 @@ export class Controller extends WorkbenchTreeController { result.push(...actions); result.push(new Separator()); } + result.pop(); // remove last separator return result; } diff --git a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts index 55577952d80..4e35e7e3ccb 100644 --- a/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts +++ b/src/vs/workbench/parts/markers/electron-browser/markersTreeViewer.ts @@ -19,6 +19,10 @@ 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 } 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 { ILabelService } from 'vs/platform/label/common/label'; +import { dirname } from 'vs/base/common/resources'; interface IResourceMarkersTemplateData { resourceLabel: ResourceLabel; @@ -28,6 +32,7 @@ interface IResourceMarkersTemplateData { interface IMarkerTemplateData { icon: HTMLElement; + actionBar: ActionBar; source: HighlightedLabel; description: HighlightedLabel; lnCol: HTMLElement; @@ -92,8 +97,10 @@ export class Renderer implements IRenderer { private static readonly RELATED_INFO_TEMPLATE_ID = 'related-info-template'; constructor( + private actionItemProvider: IActionItemProvider, @IInstantiationService private instantiationService: IInstantiationService, - @IThemeService private themeService: IThemeService + @IThemeService private themeService: IThemeService, + @ILabelService private labelService: ILabelService ) { } @@ -161,6 +168,9 @@ export class Renderer implements IRenderer { private renderRelatedInfoTemplate(container: HTMLElement): IRelatedInformationTemplateData { const data: IRelatedInformationTemplateData = Object.create(null); + dom.append(container, dom.$('.actions')); + dom.append(container, dom.$('.icon')); + data.resourceLabel = new HighlightedLabel(dom.append(container, dom.$('.related-info-resource'))); data.lnCol = dom.append(container, dom.$('span.marker-line')); @@ -174,7 +184,9 @@ export class Renderer implements IRenderer { private renderMarkerTemplate(container: HTMLElement): IMarkerTemplateData { const data: IMarkerTemplateData = Object.create(null); - data.icon = dom.append(container, dom.$('.marker-icon')); + const actionsContainer = dom.append(container, dom.$('.actions')); + data.actionBar = new ActionBar(actionsContainer, { actionItemProvider: this.actionItemProvider }); + data.icon = dom.append(container, dom.$('.icon')); data.source = new HighlightedLabel(dom.append(container, dom.$(''))); data.description = new HighlightedLabel(dom.append(container, dom.$('.marker-description'))); data.lnCol = dom.append(container, dom.$('span.marker-line')); @@ -197,27 +209,33 @@ export class Renderer implements IRenderer { if (templateData.resourceLabel instanceof FileLabel) { templateData.resourceLabel.setFile(element.uri, { matches: element.uriMatches }); } else { - templateData.resourceLabel.setLabel({ name: element.name, description: element.uri.toString(), resource: element.uri }, { matches: element.uriMatches }); + templateData.resourceLabel.setLabel({ name: element.name, description: this.labelService.getUriLabel(dirname(element.uri), true), resource: element.uri }, { matches: element.uriMatches }); } (templateData).count.setCount(element.filteredCount); } private renderMarkerElement(tree: ITree, element: Marker, templateData: IMarkerTemplateData) { let marker = element.raw; + templateData.icon.className = 'icon ' + Renderer.iconClassNameFor(marker); templateData.source.set(marker.source, element.sourceMatches); dom.toggleClass(templateData.source.element, 'marker-source', !!marker.source); + templateData.actionBar.clear(); + const quickFixAction = this.instantiationService.createInstance(QuickFixAction, element); + templateData.actionBar.push([quickFixAction], { icon: true, label: false }); + templateData.description.set(marker.message, element.messageMatches); templateData.description.element.title = marker.message; templateData.lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(marker.startLineNumber, marker.startColumn); + } private renderRelatedInfoElement(tree: ITree, element: RelatedInformation, templateData: IRelatedInformationTemplateData) { templateData.resourceLabel.set(paths.basename(element.raw.resource.fsPath), element.uriMatches); - templateData.resourceLabel.element.title = element.raw.resource.toString(); + templateData.resourceLabel.element.title = this.labelService.getUriLabel(element.raw.resource, true); templateData.lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(element.raw.startLineNumber, element.raw.startColumn); templateData.description.set(element.raw.message, element.messageMatches); templateData.description.element.title = element.raw.message; @@ -244,6 +262,7 @@ export class Renderer implements IRenderer { } else if (templateId === Renderer.MARKER_TEMPLATE_ID) { (templateData).description.dispose(); (templateData).source.dispose(); + (templateData).actionBar.dispose(); } else if (templateId === Renderer.RELATED_INFO_TEMPLATE_ID) { (templateData).description.dispose(); (templateData).resourceLabel.dispose(); @@ -253,9 +272,15 @@ export class Renderer implements IRenderer { export class MarkersTreeAccessibilityProvider implements IAccessibilityProvider { + constructor( + @ILabelService private labelServie: ILabelService + ) { + } + public getAriaLabel(tree: ITree, element: any): string { if (element instanceof ResourceMarkers) { - return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.name, element.filteredCount); + const path = this.labelServie.getUriLabel(element.uri, true) || element.uri.fsPath; + return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.filteredCount, element.name, paths.dirname(path)); } if (element instanceof Marker) { return Messages.MARKERS_TREE_ARIA_LABEL_MARKER(element); @@ -266,4 +291,3 @@ export class MarkersTreeAccessibilityProvider implements IAccessibilityProvider return null; } } - diff --git a/src/vs/workbench/parts/markers/electron-browser/media/lightbulb-dark.svg b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb-dark.svg new file mode 100644 index 00000000000..520f78f3e55 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg new file mode 100644 index 00000000000..b3596046616 --- /dev/null +++ b/src/vs/workbench/parts/markers/electron-browser/media/lightbulb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/media/markers.css b/src/vs/workbench/parts/markers/electron-browser/media/markers.css index 1aa1fecffe9..635af71f10c 100644 --- a/src/vs/workbench/parts/markers/electron-browser/media/markers.css +++ b/src/vs/workbench/parts/markers/electron-browser/media/markers.css @@ -3,30 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-action-bar .action-item.markers-panel-action-filter { +.monaco-action-bar .markers-panel-action-filter-container { cursor: default; margin-right: 10px; min-width: 150px; max-width: 500px; display: flex; - align-items: center; } -.monaco-action-bar .action-item.markers-panel-action-filter { +.monaco-action-bar .markers-panel-action-filter-container { flex: 0.7; } -.monaco-action-bar .action-item.markers-panel-action-filter.small { +.monaco-action-bar .markers-panel-action-filter-container.small { flex: 0.5; } -.monaco-action-bar .action-item.markers-panel-action-filter .monaco-inputbox { +.monaco-action-bar .markers-panel-action-filter { + display: flex; + align-items: center; + flex: 1; +} + +.monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 24px; font-size: 12px; flex: 1; } -.vs .monaco-action-bar .action-item.markers-panel-action-filter .monaco-inputbox { +.vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 25px; border: 1px solid #ddd; } @@ -47,7 +52,7 @@ } .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.hidden, -.markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge.small { +.markers-panel-action-filter-container.small .markers-panel-action-filter > .markers-panel-filter-controls > .markers-panel-filter-badge { display: none; } @@ -67,6 +72,7 @@ .markers-panel .markers-panel-container .message-box-container { line-height: 22px; padding-left: 20px; + height: 100%; } .markers-panel .markers-panel-container .message-box-container .messageAction { @@ -79,8 +85,7 @@ display: none; } -.markers-panel .markers-panel-container .tree-container.hidden, -.markers-panel .markers-panel-container .message-box-container.hidden { +.markers-panel .markers-panel-container .tree-container.hidden { display: none; visibility: hidden; } @@ -132,7 +137,7 @@ font-weight: bold; } -.markers-panel .icon { +.markers-panel .monaco-tree .markers-panel-tree-entry > .icon { height: 22px; margin-right: 6px; flex: 0 0 16px; @@ -160,4 +165,38 @@ .vs-dark .markers-panel .icon.info { background: url('status-info-inverse.svg') center center no-repeat; +} + +.markers-panel .monaco-tree .markers-panel-tree-entry .actions .action-label.icon.markers-panel-action-quickfix { + background: url('lightbulb.svg') center/80% no-repeat; + margin-right: 0px; +} + +.vs-dark .markers-panel .monaco-tree .markers-panel-tree-entry .actions .action-label.icon.markers-panel-action-quickfix { + background: url('lightbulb-dark.svg') center/80% no-repeat; +} + +.markers-panel .monaco-tree .monaco-tree-row .markers-panel-tree-entry > .actions { + width: 16px; +} + +.markers-panel .monaco-tree .monaco-tree-row .markers-panel-tree-entry > .actions .monaco-action-bar { + display: none; +} + +.markers-panel .monaco-tree .monaco-tree-row:hover .markers-panel-tree-entry > .actions .monaco-action-bar, +.markers-panel .monaco-tree .monaco-tree-row.selected .markers-panel-tree-entry > .actions .monaco-action-bar, +.markers-panel .monaco-tree .monaco-tree-row.focused .markers-panel-tree-entry > .actions .monaco-action-bar { + display: block; +} + +.markers-panel .monaco-tree .markers-panel-tree-entry .actions .action-label { + width: 16px; + height: 100%; + background-position: 50% 50%; + background-repeat: no-repeat; +} + +.markers-panel .monaco-tree .markers-panel-tree-entry .actions .action-item.disabled { + display: none; } \ No newline at end of file diff --git a/src/vs/workbench/parts/markers/electron-browser/messages.ts b/src/vs/workbench/parts/markers/electron-browser/messages.ts index 7368df79ae4..09e3eddbb5d 100644 --- a/src/vs/workbench/parts/markers/electron-browser/messages.ts +++ b/src/vs/workbench/parts/markers/electron-browser/messages.ts @@ -16,7 +16,7 @@ export default class Messages { public static MARKERS_PANEL_SHOW_LABEL: string = nls.localize('problems.view.focus.label', "Focus Problems (Errors, Warnings, Infos)"); public static PROBLEMS_PANEL_CONFIGURATION_TITLE: string = nls.localize('problems.panel.configuration.title', "Problems View"); - public static PROBLEMS_PANEL_CONFIGURATION_AUTO_REVEAL: string = nls.localize('problems.panel.configuration.autoreveal', "Controls if Problems view should automatically reveal files when opening them"); + public static PROBLEMS_PANEL_CONFIGURATION_AUTO_REVEAL: string = nls.localize('problems.panel.configuration.autoreveal', "Controls whether Problems view should automatically reveal files when opening them."); public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); public static MARKERS_PANEL_ARIA_LABEL_PROBLEMS_TREE: string = nls.localize('markers.panel.aria.label.problems.tree', "Problems grouped by files"); @@ -28,6 +28,7 @@ export default class Messages { public static MARKERS_PANEL_ACTION_TOOLTIP_USE_FILES_EXCLUDE: string = nls.localize('markers.panel.action.useFilesExclude', "Filter using Files Exclude Setting"); public static MARKERS_PANEL_ACTION_TOOLTIP_DO_NOT_USE_FILES_EXCLUDE: string = nls.localize('markers.panel.action.donotUseFilesExclude', "Do not use Files Exclude Setting"); public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems"); + public static MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX: string = nls.localize('markers.panel.action.quickfix', "Show fixes"); public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems"); public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter. Eg: text, **/*.ts, !**/node_modules/**"); public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors"); @@ -45,7 +46,7 @@ export default class Messages { public static readonly MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "({0}, {1})", '' + ln, '' + col); }; - public static readonly MARKERS_TREE_ARIA_LABEL_RESOURCE = (fileName: string, noOfProblems: number): string => { return nls.localize('problems.tree.aria.label.resource', "{0} with {1} problems", fileName, noOfProblems); }; + public static readonly MARKERS_TREE_ARIA_LABEL_RESOURCE = (noOfProblems: number, fileName: string, folder: string): string => { return nls.localize('problems.tree.aria.label.resource', "{0} problems in file {1} of folder {2}", noOfProblems, fileName, folder); }; public static readonly MARKERS_TREE_ARIA_LABEL_MARKER = (marker: Marker): string => { const relatedInformationMessage = marker.resourceRelatedInformation.length ? nls.localize('problems.tree.aria.label.marker.relatedInformation', " This problem has references to {0} locations.", marker.resourceRelatedInformation.length) : ''; switch (marker.raw.severity) { diff --git a/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts index 7ed878b7ed2..687d1e183a7 100644 --- a/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts +++ b/src/vs/workbench/parts/markers/test/electron-browser/markersModel.test.ts @@ -6,9 +6,11 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/parts/markers/electron-browser/markersModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; class TestMarkersModel extends MarkersModel { @@ -22,6 +24,12 @@ class TestMarkersModel extends MarkersModel { suite('MarkersModel Test', () => { + let instantiationService: IInstantiationService; + + setup(() => { + instantiationService = workbenchInstantiationService(); + }); + test('getFilteredResource return markers grouped by resource', function () { const marker1 = aMarker('res1'); const marker2 = aMarker('res2'); @@ -29,7 +37,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('res2'); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.filteredResources; @@ -61,7 +69,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('b/res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('c/res2', MarkerSeverity.Info); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.resources; @@ -80,7 +88,7 @@ suite('MarkersModel Test', () => { const marker4 = aMarker('b/res3'); const marker5 = aMarker('res4'); const marker6 = aMarker('c/res2'); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6]); const actuals = testObject.resources; @@ -108,7 +116,7 @@ suite('MarkersModel Test', () => { const marker13 = aWarningWithRange(5); const marker14 = anErrorWithRange(4); const marker15 = anErrorWithRange(8, 2, 8, 4); - const testObject = new TestMarkersModel([marker1, marker2, marker3, marker4, marker5, marker6, marker7, marker8, marker9, marker10, marker11, marker12, marker13, marker14, marker15]); + const testObject = instantiationService.createInstance(TestMarkersModel, [marker1, marker2, marker3, marker4, marker5, marker6, marker7, marker8, marker9, marker10, marker11, marker12, marker13, marker14, marker15]); const actuals = testObject.resources[0].markers; @@ -132,19 +140,19 @@ suite('MarkersModel Test', () => { test('toString()', function () { let marker = aMarker('a/res1'); marker.code = '1234'; - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker, null).toString()); marker = aMarker('a/res2', MarkerSeverity.Warning); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker, null).toString()); marker = aMarker('a/res2', MarkerSeverity.Info, 1, 2, 1, 8, 'Info', ''); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker, null).toString()); marker = aMarker('a/res2', MarkerSeverity.Hint, 1, 2, 1, 8, 'Ignore message', 'Ignore'); - assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), new Marker('', marker).toString()); + assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path }, null, '\t'), instantiationService.createInstance(Marker, '', marker, null).toString()); marker = aMarker('a/res2', MarkerSeverity.Warning, 1, 2, 1, 8, 'Warning message', '', [{ startLineNumber: 2, startColumn: 5, endLineNumber: 2, endColumn: 10, message: 'some info', resource: URI.file('a/res3') }]); - const testObject = new Marker('', marker); + const testObject = instantiationService.createInstance(Marker, '', marker, null); testObject.resourceRelatedInformation = marker.relatedInformation.map(r => new RelatedInformation('', r)); assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path, relatedInformation: marker.relatedInformation.map(r => ({ ...r, resource: r.resource.path })) }, null, '\t'), testObject.toString()); }); diff --git a/src/vs/workbench/parts/navigation/common/navigation.contribution.ts b/src/vs/workbench/parts/navigation/common/navigation.contribution.ts new file mode 100644 index 00000000000..87699346f17 --- /dev/null +++ b/src/vs/workbench/parts/navigation/common/navigation.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { HistoryNavigationKeybindingsChangedContribution } from 'vs/workbench/parts/navigation/common/removedKeybindingsContribution'; + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(HistoryNavigationKeybindingsChangedContribution, LifecyclePhase.Eventually); \ No newline at end of file diff --git a/src/vs/workbench/parts/navigation/common/removedKeybindingsContribution.ts b/src/vs/workbench/parts/navigation/common/removedKeybindingsContribution.ts new file mode 100644 index 00000000000..625bd33cf6c --- /dev/null +++ b/src/vs/workbench/parts/navigation/common/removedKeybindingsContribution.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { localize } from 'vs/nls'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; + +// TODO@Sandeep remove me after a while +export class HistoryNavigationKeybindingsChangedContribution implements IWorkbenchContribution { + + private previousCommands: string[] = [ + + 'search.history.showNextIncludePattern', + 'search.history.showPreviousIncludePattern', + 'search.history.showNextExcludePattern', + 'search.history.showPreviousExcludePattern', + 'search.history.showNext', + 'search.history.showPrevious', + 'search.replaceHistory.showNext', + 'search.replaceHistory.showPrevious', + + 'find.history.showPrevious', + 'find.history.showNext', + + 'workbench.action.terminal.findWidget.history.showNext', + 'workbench.action.terminal.findWidget.history.showPrevious', + + 'editor.action.extensioneditor.showNextFindTerm', + 'editor.action.extensioneditor.showPreviousFindTerm', + + 'editor.action.webvieweditor.showNextFindTerm', + 'editor.action.webvieweditor.showPreviousFindTerm', + + 'repl.action.historyNext', + 'repl.action.historyPrevious' + ]; + + constructor( + @INotificationService private readonly notificationService: INotificationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IStorageService private readonly storageService: IStorageService + ) { + this.showRemovedWarning(); + } + + private showRemovedWarning(): void { + const key = 'donotshow.historyNavigation.warning'; + if (!this.storageService.getBoolean(key, StorageScope.GLOBAL, false)) { + const keybindingsToRemove = this.keybindingService.getKeybindings().filter(keybinding => !keybinding.isDefault && this.previousCommands.indexOf(keybinding.command) !== -1); + if (keybindingsToRemove.length) { + const message = localize('showDeprecatedWarningMessage', "History navigation commands have changed. Please update your keybindings to use following new commands: 'history.showPrevious' and 'history.showNext'"); + this.notificationService.prompt(Severity.Warning, message, [ + { + label: localize('Open Keybindings', "Open Keybindings File"), + run: () => this.preferencesService.openGlobalKeybindingSettings(true) + }, + { + label: localize('more information', "More Information..."), + run: () => null + }, + { + label: localize('Do not show again', "Don't show again"), + isSecondary: true, + run: () => this.storageService.store(key, true, StorageScope.GLOBAL) + } + ]); + } + } + } +} diff --git a/src/vs/workbench/parts/outline/electron-browser/media/symbol-icons.css b/src/vs/workbench/parts/outline/electron-browser/media/symbol-icons.css deleted file mode 100644 index 2369d98e0ed..00000000000 --- a/src/vs/workbench/parts/outline/electron-browser/media/symbol-icons.css +++ /dev/null @@ -1,181 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .symbol-icon { - display: inline-block; - height: 14px; - width: 16px; - min-height: 14px; - min-width: 16px; -} - -.monaco-workbench .symbol-icon.constant { - background-image: url('Constant_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.constant, .hc-black .monaco-workbench .symbol-icon.constant { - background-image: url('Constant_16x_inverse.svg'); -} - -.monaco-workbench .symbol-icon.enum-member { - background-image: url('EnumItem_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.enum-member, .hc-black .monaco-workbench .symbol-icon.enum-member { - background-image: url('EnumItem_inverse_16x.svg'); -} - -.monaco-workbench .symbol-icon.struct { - background-image: url('Structure_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.struct, .hc-black .monaco-workbench .symbol-icon.struct { - background-image: url('Structure_16x_vscode_inverse.svg'); -} - -.monaco-workbench .symbol-icon.event { - background-image: url('Event_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.event, .hc-black .monaco-workbench .symbol-icon.event { - background-image: url('Event_16x_vscode_inverse.svg'); -} - -.monaco-workbench .symbol-icon.operator { - background-image: url('Operator_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.operator, .hc-black .monaco-workbench .symbol-icon.operator { - background-image: url('Operator_16x_vscode_inverse.svg'); -} - -.monaco-workbench .symbol-icon.type-parameter { - background-image: url('Template_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} - -.vs-dark .monaco-workbench .symbol-icon.type-parameter, .hc-black .monaco-workbench .symbol-icon.type-parameter { - background-image: url('Template_16x_vscode_inverse.svg'); -} - -.monaco-workbench .symbol-icon.method, .monaco-workbench .symbol-icon.function, .monaco-workbench .symbol-icon.constructor, .monaco-workbench .symbol-icon.field, .monaco-workbench .symbol-icon.variable, .monaco-workbench .symbol-icon.class, .monaco-workbench .symbol-icon.interface, .monaco-workbench .symbol-icon.object, .monaco-workbench .symbol-icon.namespace, .monaco-workbench .symbol-icon.package, .monaco-workbench .symbol-icon.module, .monaco-workbench .symbol-icon.property, .monaco-workbench .symbol-icon.enum, .monaco-workbench .symbol-icon.key, .monaco-workbench .symbol-icon.string, .monaco-workbench .symbol-icon.rule, .monaco-workbench .symbol-icon.file, .monaco-workbench .symbol-icon.array, .monaco-workbench .symbol-icon.number, .monaco-workbench .symbol-icon.null, .monaco-workbench .symbol-icon.boolean { - background-image: url('symbol-sprite.svg'); - background-repeat: no-repeat; -} - -.vs .monaco-workbench .symbol-icon.method, .vs .monaco-workbench .symbol-icon.function, .vs .monaco-workbench .symbol-icon.constructor { - background-position: 0 -4px; -} - -.vs .monaco-workbench .symbol-icon.field, .vs .monaco-workbench .symbol-icon.variable { - background-position: -22px -4px; -} - -.vs .monaco-workbench .symbol-icon.class { - background-position: -43px -3px; -} - -.vs .monaco-workbench .symbol-icon.interface { - background-position: -63px -4px; -} - -.vs .monaco-workbench .symbol-icon.object, .vs .monaco-workbench .symbol-icon.namespace, .vs .monaco-workbench .symbol-icon.package, .vs .monaco-workbench .symbol-icon.module { - background-position: -82px -4px; -} - -.vs .monaco-workbench .symbol-icon.property { - background-position: -102px -3px; -} - -.vs .monaco-workbench .symbol-icon.enum { - background-position: -122px -3px; -} - -.vs .monaco-workbench .symbol-icon.key, .vs .monaco-workbench .symbol-icon.string { - background-position: -202px -3px; -} - -.vs .monaco-workbench .symbol-icon.rule { - background-position: -242px -4px; -} - -.vs .monaco-workbench .symbol-icon.file { - background-position: -262px -4px; -} - -.vs .monaco-workbench .symbol-icon.array { - background-position: -302px -4px; -} - -.vs .monaco-workbench .symbol-icon.number { - background-position: -322px -4px; -} - -.vs .monaco-workbench .symbol-icon.null, .vs .monaco-workbench .symbol-icon.boolean { - background-position: -343px -4px; -} - -.vs-dark .monaco-workbench .symbol-icon.method, .vs-dark .monaco-workbench .symbol-icon.function, .vs-dark .monaco-workbench .symbol-icon.constructor, .hc-black .monaco-workbench .symbol-icon.method, .hc-black .monaco-workbench .symbol-icon.function, .hc-black .monaco-workbench .symbol-icon.constructor { - background-position: 0 -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.field, .hc-black .monaco-workbench .symbol-icon.field, .vs-dark .monaco-workbench .symbol-icon.variable, .hc-black .monaco-workbench .symbol-icon.variable { - background-position: -22px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.class, .hc-black .monaco-workbench .symbol-icon.class { - background-position: -43px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.interface, .hc-black .monaco-workbench .symbol-icon.interface { - background-position: -63px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.object, .vs-dark .monaco-workbench .symbol-icon.namespace, .vs-dark .monaco-workbench .symbol-icon.package, .vs-dark .monaco-workbench .symbol-icon.module, .hc-black .monaco-workbench .symbol-icon.object, .hc-black .monaco-workbench .symbol-icon.namespace, .hc-black .monaco-workbench .symbol-icon.package, .hc-black .monaco-workbench .symbol-icon.module { - background-position: -82px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.property, .hc-black .monaco-workbench .symbol-icon.property { - background-position: -102px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.key, .vs-dark .monaco-workbench .symbol-icon.string, .hc-black .monaco-workbench .symbol-icon.key, .hc-black .monaco-workbench .symbol-icon.string { - background-position: -202px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.enum, .hc-black .monaco-workbench .symbol-icon.enum { - background-position: -122px -23px; -} - -.vs-dark .monaco-workbench .symbol-icon.rule, .hc-black .monaco-workbench .symbol-icon.rule { - background-position: -242px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.file, .hc-black .monaco-workbench .symbol-icon.file { - background-position: -262px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.array, .hc-black .monaco-workbench .symbol-icon.array { - background-position: -302px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.number, .hc-black .monaco-workbench .symbol-icon.number { - background-position: -322px -24px; -} - -.vs-dark .monaco-workbench .symbol-icon.null, .vs-dark .monaco-workbench .symbol-icon.boolean, .hc-black .monaco-workbench .symbol-icon.null, .hc-black .monaco-workbench .symbol-icon.boolean { - background-position: -342px -24px; -} diff --git a/src/vs/workbench/parts/outline/electron-browser/media/symbol-sprite.svg b/src/vs/workbench/parts/outline/electron-browser/media/symbol-sprite.svg deleted file mode 100644 index ee9a63dcf6f..00000000000 --- a/src/vs/workbench/parts/outline/electron-browser/media/symbol-sprite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/outline/electron-browser/outline.contribution.ts b/src/vs/workbench/parts/outline/electron-browser/outline.contribution.ts index b19d7aecf51..f55c83c680d 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outline.contribution.ts +++ b/src/vs/workbench/parts/outline/electron-browser/outline.contribution.ts @@ -12,10 +12,10 @@ import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { VIEW_CONTAINER } from 'vs/workbench/parts/files/common/files'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { OutlineConfigKeys } from 'vs/workbench/parts/outline/electron-browser/outline'; +import { OutlineConfigKeys, OutlineViewId } from 'vs/workbench/parts/outline/electron-browser/outline'; const _outlineDesc = { - id: 'code.outline', + id: OutlineViewId, name: localize('name', "Outline"), ctor: OutlinePanel, container: VIEW_CONTAINER, @@ -30,7 +30,7 @@ ViewsRegistry.registerViews([_outlineDesc]); CommandsRegistry.registerCommand('outline.focus', accessor => { let viewsService = accessor.get(IViewsService); - return viewsService.openView(_outlineDesc.id, true); + return viewsService.openView(OutlineViewId, true); }); MenuRegistry.addCommand({ @@ -42,34 +42,29 @@ MenuRegistry.addCommand({ Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ 'id': 'outline', 'order': 117, + 'title': localize('outlineConfigurationTitle', "Outline"), 'type': 'object', 'properties': { + [OutlineConfigKeys.icons]: { + 'description': localize('outline.showIcons', "Render Outline Elements with Icons."), + 'type': 'boolean', + 'default': true + }, [OutlineConfigKeys.problemsEnabled]: { 'description': localize('outline.showProblem', "Show Errors & Warnings on Outline Elements."), 'type': 'boolean', 'default': true - } - } -}); - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - 'id': 'outline', - 'order': 117, - 'type': 'object', - 'properties': { + }, + [OutlineConfigKeys.problemsEnabled]: { + 'description': localize('outline.showProblem', "Show Errors & Warnings on Outline Elements."), + 'type': 'boolean', + 'default': true + }, [OutlineConfigKeys.problemsColors]: { 'description': localize('outline.problem.colors', "Use colors for Errors & Warnings."), 'type': 'boolean', 'default': true - } - } -}); - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - 'id': 'outline', - 'order': 117, - 'type': 'object', - 'properties': { + }, [OutlineConfigKeys.problemsBadges]: { 'description': localize('outline.problems.badges', "Use badges for Errors & Warnings."), 'type': 'boolean', diff --git a/src/vs/workbench/parts/outline/electron-browser/outline.ts b/src/vs/workbench/parts/outline/electron-browser/outline.ts index d68cfaf67b9..1c48fb2e103 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outline.ts +++ b/src/vs/workbench/parts/outline/electron-browser/outline.ts @@ -2,11 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - 'use strict'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export enum OutlineConfigKeys { +export const OutlineViewId = 'outline'; + +export const OutlineViewFiltered = new RawContextKey('outlineFiltered', false); +export const OutlineViewFocused = new RawContextKey('outlineFocused', false); + +export const enum OutlineConfigKeys { + 'icons' = 'outline.icons', 'problemsEnabled' = 'outline.problems.enabled', 'problemsColors' = 'outline.problems.colors', 'problemsBadges' = 'outline.problems.badges' diff --git a/src/vs/workbench/parts/outline/electron-browser/outlineModel.ts b/src/vs/workbench/parts/outline/electron-browser/outlineModel.ts deleted file mode 100644 index 9497ac83d85..00000000000 --- a/src/vs/workbench/parts/outline/electron-browser/outlineModel.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { DocumentSymbolProviderRegistry, SymbolInformation, DocumentSymbolProvider } from 'vs/editor/common/modes'; -import { ITextModel } from 'vs/editor/common/model'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; -import { IPosition } from 'vs/editor/common/core/position'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { first, size } from 'vs/base/common/collections'; -import { isFalsyOrEmpty, binarySearch } from 'vs/base/common/arrays'; -import { commonPrefixLength } from 'vs/base/common/strings'; -import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; - -export abstract class TreeElement { - abstract id: string; - abstract children: { [id: string]: TreeElement }; - abstract parent: TreeElement | any; - - static findId(candidate: SymbolInformation | string, container: TreeElement): string { - // complex id-computation which contains the origin/extension, - // the parent path, and some dedupe logic when names collide - let candidateId: string; - if (typeof candidate === 'string') { - candidateId = `${container.id}/${candidate}`; - } else { - candidateId = `${container.id}/${candidate.name}`; - if (container.children[candidateId] !== void 0) { - candidateId = `${container.id}/${candidate.name}_${candidate.definingRange.startLineNumber}_${candidate.definingRange.startColumn}`; - } - } - - let id = candidateId; - for (let i = 0; container.children[id] !== void 0; i++) { - id = `${candidateId}_${i}`; - } - - return id; - } - - static getElementById(id: string, element: TreeElement): TreeElement { - if (!id) { - return undefined; - } - let len = commonPrefixLength(id, element.id); - if (len === id.length) { - return element; - } - if (len < element.id.length) { - return undefined; - } - for (const key in element.children) { - let candidate = TreeElement.getElementById(id, element.children[key]); - if (candidate) { - return candidate; - } - } - return undefined; - } - - static size(element: TreeElement): number { - let res = 1; - for (const key in element.children) { - res += TreeElement.size(element.children[key]); - } - return res; - } -} - -export class OutlineElement extends TreeElement { - - children: { [id: string]: OutlineElement; } = Object.create(null); - score: FuzzyScore = [0, []]; - marker: { count: number, topSev: MarkerSeverity }; - - constructor( - readonly id: string, - public parent: OutlineModel | OutlineGroup | OutlineElement, - readonly symbol: SymbolInformation - ) { - super(); - } -} - -export class OutlineGroup extends TreeElement { - - children: { [id: string]: OutlineElement; } = Object.create(null); - - constructor( - readonly id: string, - public parent: OutlineModel, - readonly provider: DocumentSymbolProvider, - readonly providerIndex: number, - ) { - super(); - } - - updateMatches(pattern: string, topMatch: OutlineElement): OutlineElement { - 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 { - item.score = fuzzyScore(pattern, item.symbol.name, undefined, true); - if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) { - topMatch = item; - } - for (const key in item.children) { - let child = item.children[key]; - topMatch = this._updateMatches(pattern, child, topMatch); - if (!item.score && child.score) { - // don't filter parents with unfiltered children - item.score = [0, []]; - } - } - return topMatch; - } - - getItemEnclosingPosition(position: IPosition): OutlineElement { - return this._getItemEnclosingPosition(position, this.children); - } - - private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement { - for (let key in children) { - let item = children[key]; - if (!Range.containsPosition(item.symbol.definingRange || item.symbol.location.range, position)) { - continue; - } - return this._getItemEnclosingPosition(position, item.children) || item; - } - return undefined; - } - - updateMarker(marker: IMarker[]): void { - for (const key in this.children) { - this._updateMarker(marker, this.children[key]); - } - } - - private _updateMarker(marker: IMarker[], item: OutlineElement): void { - let idx = binarySearch(marker, item.symbol.definingRange, Range.compareRangesUsingStarts); - let start: number; - if (idx < 0) { - // ~idx is the index at which the symbol should be... start search from there - start = ~idx; - if (start > 0 && Range.areIntersecting(marker[start - 1], item.symbol.definingRange)) { - start -= 1; - } - } else { - start = idx; - } - - let myMarkers: IMarker[] = []; - let myTopSev: MarkerSeverity; - - while (start < marker.length) { - if (!Range.areIntersecting(marker[start], item.symbol.definingRange)) { - break; - } - // this marker belongs to this element and it takes it away. - // children of this marker might take it away again tho... - let myMarker = marker.splice(start, 1)[0]; - myMarkers.push(myMarker); - if (!myTopSev || myMarker.severity > myTopSev) { - myTopSev = myMarker.severity; - } - } - - // recursivion into children. this might cause myMarkers to become empty - // and because of that we store the top marker to which tell me what the - // most severe marker of my children is - for (const key in item.children) { - this._updateMarker(myMarkers, item.children[key]); - } - - if (!myTopSev) { - item.marker = undefined; - } else { - item.marker = { - count: myMarkers.length, - topSev: myTopSev - }; - } - - } -} - -export class OutlineModel extends TreeElement { - - static create(textModel: ITextModel): TPromise { - let result = new OutlineModel(textModel); - let promises = DocumentSymbolProviderRegistry.ordered(textModel).map((provider, index) => { - - let id = TreeElement.findId(`provider_${index}`, result); - let group = new OutlineGroup(id, result, provider, index); - - return asWinJsPromise(token => provider.provideDocumentSymbols(result.textModel, token)).then(result => { - if (!isFalsyOrEmpty(result)) { - for (const info of result) { - OutlineModel._makeOutlineElement(info, group); - } - } - return group; - }, err => { - //todo@joh capture error in group - return group; - }).then(group => { - result._groups[id] = group; - }); - }); - - return TPromise.join(promises).then(() => { - - let count = 0; - for (const key in result._groups) { - let group = result._groups[key]; - if (first(group.children) === undefined) { // empty - delete result._groups[key]; - } else { - count += 1; - } - } - - if (count !== 1) { - // - result.children = result._groups; - - } else { - // adopt all elements of the first group - let group = first(result._groups); - for (let key in group.children) { - let child = group.children[key]; - child.parent = result; - result.children[child.id] = child; - } - } - - return result; - }); - } - - private static _makeOutlineElement(info: SymbolInformation, container: OutlineGroup | OutlineElement): void { - let id = TreeElement.findId(info, container); - let res = new OutlineElement(id, container, info); - if (info.children) { - for (const childInfo of info.children) { - OutlineModel._makeOutlineElement(childInfo, res); - } - } - container.children[res.id] = res; - } - - readonly id = 'root'; - readonly parent = undefined; - - private _groups: { [id: string]: OutlineGroup; } = Object.create(null); - children: { [id: string]: OutlineGroup | OutlineElement; } = Object.create(null); - - private constructor(readonly textModel: ITextModel) { - super(); - } - - dispose(): void { - - } - - adopt(other: OutlineModel): boolean { - if (this.textModel.uri.toString() !== other.textModel.uri.toString()) { - return false; - } - if (size(this._groups) !== size(other._groups)) { - return false; - } - this._groups = other._groups; - this.children = other.children; - return true; - } - - updateMatches(pattern: string): OutlineElement { - let topMatch: OutlineElement; - for (const key in this._groups) { - topMatch = this._groups[key].updateMatches(pattern, topMatch); - } - return topMatch; - } - - getItemEnclosingPosition(position: IPosition): OutlineElement { - for (const key in this._groups) { - let result = this._groups[key].getItemEnclosingPosition(position); - if (result) { - return result; - } - } - return undefined; - } - - getItemById(id: string): TreeElement { - return TreeElement.getElementById(id, this); - } - - updateMarker(marker: IMarker[]): void { - // sort markers by start range so that we can use - // outline element starts for quicker look up - marker.sort(Range.compareRangesUsingStarts); - - for (const key in this._groups) { - this._groups[key].updateMarker(marker); - } - } -} diff --git a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css index c90e9218bf5..23afd397253 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css +++ b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.css @@ -8,19 +8,10 @@ flex-direction: column; } -.monaco-workbench .outline-panel .outline-input { - box-sizing: border-box; - padding: 5px 9px 2px 9px; -} - -.monaco-workbench .outline-panel .outline-input .monaco-inputbox { - width: 100%; -} - .monaco-workbench .outline-panel .outline-progress { width: 100%; height: 2px; - padding-bottom: 5px; + padding-bottom: 3px; } .monaco-workbench .outline-panel .outline-progress .monaco-progress-container { @@ -31,6 +22,29 @@ height: 2px; } +.monaco-workbench .outline-panel .outline-input { + box-sizing: border-box; + padding: 2px 9px 5px 9px; + position: relative; +} + +.monaco-workbench .outline-panel .outline-input .monaco-inputbox { + width: 100%; +} + +.monaco-workbench .outline-panel .outline-input .monaco-inputbox .input { + padding-right: 22px; +} + +.monaco-workbench .outline-panel .outline-input .monaco-custom-checkbox { + position: absolute; + top: 5px; + right: 13px; + bottom: 0; + display: flex; + align-items: center; +} + .monaco-workbench .outline-panel .outline-tree { height: 100%; } @@ -57,44 +71,17 @@ display: none; } -.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration { +.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration{ /* make sure selection color wins when a label is being selected */ color: inherit !important; } -.monaco-workbench .outline-panel .outline-element { - display: flex; - flex: 1; - flex-flow: row nowrap; - align-items: center; +.monaco-tree.focused .selected .outline-element-label .monaco-highlighted-label .highlight, +.monaco-tree.focused .selected .monaco-icon-label .monaco-highlighted-label .highlight{ + /* allows text color to use the default when selected */ + color: inherit !important; } -.monaco-workbench .outline-panel .outline-element .outline-element-icon { - padding-right: 3px; -} - -.monaco-workbench .outline-panel .outline-element .outline-element-label { - text-overflow: ellipsis; - overflow: hidden; - color: var(--outline-element-color); -} - -.monaco-workbench .outline-panel .outline-element .outline-element-label .monaco-highlighted-label .highlight { - font-weight: bold; -} - -.monaco-workbench .outline-panel .outline-element .outline-element-decoration { - opacity: 0.75; - font-size: 90%; - font-weight: 600; - padding: 0 12px 0 5px; - margin-left: auto; - text-align: center; - color: var(--outline-element-color); -} - -.monaco-workbench .outline-panel .outline-element .outline-element-decoration.bubble { - font-family: octicons; - font-size: 14px; - opacity: 0.4; +.monaco-workbench .outline-panel.no-icons .outline-element .outline-element-icon { + display: none; } diff --git a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts index 5573d1a1c09..00b7b5a9bcd 100644 --- a/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts +++ b/src/vs/workbench/parts/outline/electron-browser/outlinePanel.ts @@ -4,51 +4,56 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { posix } from 'path'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Action, IAction, RadioGroup } from 'vs/base/common/actions'; +import { firstIndex } from 'vs/base/common/arrays'; +import { createCancelablePromise, TimeoutTimer } from 'vs/base/common/async'; +import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import 'vs/css!./outlinePanel'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; +import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { attachInputBoxStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { OutlineElement, OutlineModel, TreeElement } from './outlineModel'; -import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineItemCompareType, OutlineItemFilter, OutlineRenderer, OutlineTreeState } from './outlineTree'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { asDisposablePromise } from 'vs/base/common/async'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { firstIndex } from 'vs/base/common/arrays'; -import URI from 'vs/base/common/uri'; -import { OutlineConfigKeys } from './outline'; +import { CollapseAction } from 'vs/workbench/browser/viewlet'; +import { IViewsService } from 'vs/workbench/common/views'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineItemCompareType, OutlineItemFilter, OutlineRenderer, OutlineTreeState } from '../../../../editor/contrib/documentSymbols/outlineTree'; +import { OutlineConfigKeys, OutlineViewFiltered, OutlineViewFocused, OutlineViewId } from './outline'; class RequestState { @@ -130,11 +135,15 @@ class RequestOracle { let modeListener = codeEditor.onDidChangeModelLanguage(_ => { this._callback(codeEditor, undefined); }); + let disposeListener = codeEditor.onDidDispose(() => { + this._callback(undefined, undefined); + }); this._sessionDisposable = { dispose() { contentListener.dispose(); clearTimeout(handle); modeListener.dispose(); + disposeListener.dispose(); } }; } @@ -142,8 +151,8 @@ class RequestOracle { class SimpleToggleAction extends Action { - constructor(label: string, checked: boolean, callback: (action: SimpleToggleAction) => any) { - super(`simple` + defaultGenerator.nextId(), label, undefined, true, _ => { + constructor(label: string, checked: boolean, callback: (action: SimpleToggleAction) => any, className?: string) { + super(`simple` + defaultGenerator.nextId(), label, className, true, _ => { this.checked = !this.checked; callback(this); return undefined; @@ -152,12 +161,14 @@ class SimpleToggleAction extends Action { } } -class OutlineState { + +class OutlineViewState { private _followCursor = false; + private _filterOnType = true; private _sortBy = OutlineItemCompareType.ByKind; - private _onDidChange = new Emitter<{ followCursor?: boolean, sortBy?: boolean }>(); + private _onDidChange = new Emitter<{ followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }>(); readonly onDidChange = this._onDidChange.event; set followCursor(value: boolean) { @@ -171,6 +182,17 @@ class OutlineState { return this._followCursor; } + get filterOnType() { + return this._filterOnType; + } + + set filterOnType(value) { + if (value !== this._filterOnType) { + this._filterOnType = value; + this._onDidChange.fire({ filterOnType: true }); + } + } + set sortBy(value: OutlineItemCompareType) { if (value !== this._sortBy) { this._sortBy = value; @@ -207,7 +229,7 @@ export class OutlinePanel extends ViewletPanel { private _disposables = new Array(); private _editorDisposables = new Array(); - private _outlineViewState = new OutlineState(); + private _outlineViewState = new OutlineViewState(); private _requestOracle: RequestOracle; private _cachedHeight: number; private _domNode: HTMLElement; @@ -216,10 +238,15 @@ export class OutlinePanel extends ViewletPanel { private _input: InputBox; private _progressBar: ProgressBar; private _tree: WorkbenchTree; + private _treeDataSource: OutlineDataSource; + private _treeRenderer: OutlineRenderer; private _treeFilter: OutlineItemFilter; private _treeComparator: OutlineItemComparator; private _treeStates = new LRUCache(10); + private readonly _contextKeyFocused: IContextKey; + private readonly _contextKeyFiltered: IContextKey; + constructor( options: IViewletViewOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -228,50 +255,63 @@ export class OutlinePanel extends ViewletPanel { @IEditorService private readonly _editorService: IEditorService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, ) { - super(options, keybindingService, contextMenuService, configurationService); - + super(options, _keybindingService, contextMenuService, configurationService); this._outlineViewState.restore(this._storageService); + this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); + this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); + this._disposables.push(this.onDidFocus(_ => this._contextKeyFocused.set(true))); + this._disposables.push(this.onDidBlur(_ => this._contextKeyFocused.set(false))); } dispose(): void { dispose(this._disposables); dispose(this._requestOracle); + dispose(this._editorDisposables); super.dispose(); } focus(): void { if (this._tree) { + // focus on tree and fallback to root + // dom node when the tree cannot take focus, + // e.g. when hidden this._tree.domFocus(); + if (!this._tree.isDOMFocused()) { + this._domNode.focus(); + } } } protected renderBody(container: HTMLElement): void { this._domNode = container; + this._domNode.tabIndex = 0; dom.addClass(container, 'outline-panel'); + let progressContainer = dom.$('.outline-progress'); this._message = dom.$('.outline-message'); this._inputContainer = dom.$('.outline-input'); - let progressContainer = dom.$('.outline-progress'); this._progressBar = new ProgressBar(progressContainer); this.disposables.push(attachProgressBarStyler(this._progressBar, this._themeService)); let treeContainer = dom.$('.outline-tree'); dom.append( container, - this._message, this._inputContainer, progressContainer, treeContainer + progressContainer, this._message, this._inputContainer, treeContainer ); - this._input = new InputBox(this._inputContainer, null, { placeholder: localize('filter', "Filter") }); + this._input = new InputBox(this._inputContainer, null, { + placeholder: this._outlineViewState.filterOnType ? localize('filter.placeholder', "Filter") : localize('find.placeholder', "Find") + }); this._input.disable(); this.disposables.push(attachInputBoxStyler(this._input, this._themeService)); this.disposables.push(dom.addStandardDisposableListener(this._input.inputElement, 'keyup', event => { - // todo@joh make those keybindings configurable? if (event.keyCode === KeyCode.DownArrow) { this._tree.focusNext(); this._tree.domFocus(); @@ -280,7 +320,9 @@ export class OutlinePanel extends ViewletPanel { this._tree.domFocus(); } else if (event.keyCode === KeyCode.Enter) { let element = this._tree.getFocus(); - this._revealTreeSelection(element, true, false); + if (element instanceof OutlineElement) { + this._revealTreeSelection(OutlineModel.get(element), element, true, false); + } } else if (event.keyCode === KeyCode.Escape) { this._input.value = ''; this._tree.domFocus(); @@ -290,8 +332,6 @@ export class OutlinePanel extends ViewletPanel { const $this = this; const controller = new class extends OutlineController { - private readonly _mapper = KeyboardMapperFactory.INSTANCE; - constructor() { super({}, $this.configurationService); } @@ -307,32 +347,39 @@ export class OutlinePanel extends ViewletPanel { // crazy -> during keydown focus moves to the input box // and because of that the keyup event is handled by the // input field - const mapping = this._mapper.getRawKeyboardMapping(); - if (!mapping) { - return false; - } - const keyInfo = mapping[event.code]; - if (keyInfo.value) { + if ($this._keybindingService.mightProducePrintableCharacter(event)) { $this._input.focus(); return true; } return false; } }; - const dataSource = new OutlineDataSource(); - const renderer = this._instantiationService.createInstance(OutlineRenderer); + + this._treeRenderer = this._instantiationService.createInstance(OutlineRenderer); + this._treeDataSource = new OutlineDataSource(); this._treeComparator = new OutlineItemComparator(this._outlineViewState.sortBy); this._treeFilter = new OutlineItemFilter(); - this._tree = this._instantiationService.createInstance(WorkbenchTree, treeContainer, { controller, dataSource, renderer, sorter: this._treeComparator, filter: this._treeFilter }, {}); + this._tree = this._instantiationService.createInstance(WorkbenchTree, treeContainer, { controller, renderer: this._treeRenderer, dataSource: this._treeDataSource, sorter: this._treeComparator, filter: this._treeFilter }, {}); + + this._treeRenderer.renderProblemColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); + this._treeRenderer.renderProblemBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); this._disposables.push(this._tree, this._input); this._disposables.push(this._outlineViewState.onDidChange(this._onDidChangeUserState, this)); + + // feature: toggle icons + dom.toggleClass(this._domNode, 'no-icons', !this._configurationService.getValue(OutlineConfigKeys.icons)); + this.disposables.push(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.icons)) { + dom.toggleClass(this._domNode, 'no-icons', !this._configurationService.getValue(OutlineConfigKeys.icons)); + } + })); } protected layoutBody(height: number = this._cachedHeight): void { this._cachedHeight = height; this._input.layout(); - this._tree.layout(height - (dom.getTotalHeight(this._inputContainer) + 7 /*progressbar height, defined in outlinePanel.css*/)); + this._tree.layout(height - (dom.getTotalHeight(this._inputContainer) + 5 /*progressbar height, defined in outlinePanel.css*/)); } setVisible(visible: boolean): TPromise { @@ -361,7 +408,8 @@ export class OutlinePanel extends ViewletPanel { new SimpleToggleAction(localize('sortByKind', "Sort By: Type"), this._outlineViewState.sortBy === OutlineItemCompareType.ByKind, _ => this._outlineViewState.sortBy = OutlineItemCompareType.ByKind), ]); let result = [ - new SimpleToggleAction(localize('live', "Follow Cursor"), this._outlineViewState.followCursor, action => this._outlineViewState.followCursor = action.checked), + new SimpleToggleAction(localize('followCur', "Follow Cursor"), this._outlineViewState.followCursor, action => this._outlineViewState.followCursor = action.checked), + new SimpleToggleAction(localize('filterOnType', "Filter on Type"), this._outlineViewState.filterOnType, action => this._outlineViewState.filterOnType = action.checked), new Separator(), ...group.actions, ]; @@ -371,7 +419,7 @@ export class OutlinePanel extends ViewletPanel { return result; } - private _onDidChangeUserState(e: { followCursor?: boolean, sortBy?: boolean }) { + private _onDidChangeUserState(e: { followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }) { this._outlineViewState.persist(this._storageService); if (e.followCursor) { // todo@joh update immediately @@ -380,41 +428,70 @@ export class OutlinePanel extends ViewletPanel { this._treeComparator.type = this._outlineViewState.sortBy; this._tree.refresh(undefined, true); } + if (e.filterOnType) { + this._applyTypeToFilter(); + } } private _showMessage(message: string) { dom.addClass(this._domNode, 'message'); + this._tree.setInput(undefined); this._progressBar.stop().hide(); this._message.innerText = escape(message); } - private async _doUpdate(editor: ICodeEditor, event: IModelContentChangedEvent): TPromise { + private static _createOutlineModel(model: ITextModel, disposables: IDisposable[]): Promise { + let promise = createCancelablePromise(token => OutlineModel.create(model, token)); + disposables.push({ dispose() { promise.cancel(); } }); + return promise.catch(err => { + if (!isPromiseCanceledError(err)) { + throw err; + } + return undefined; + }); + } + + private async _doUpdate(editor: ICodeEditor, event: IModelContentChangedEvent): Promise { dispose(this._editorDisposables); this._editorDisposables = new Array(); - this._input.disable(); - this._input.value = ''; this._progressBar.infinite().show(150); + this._input.disable(); + if (!event) { + this._input.value = ''; + } if (!editor || !DocumentSymbolProviderRegistry.has(editor.getModel())) { return this._showMessage(localize('no-editor', "There are no editors open that can provide outline information.")); } let textModel = editor.getModel(); - let model = await asDisposablePromise(OutlineModel.create(textModel), undefined, this._editorDisposables).promise; + let loadingMessage: IDisposable; + let oldModel = this._tree.getInput(); + if (!oldModel) { + loadingMessage = new TimeoutTimer( + () => this._showMessage(localize('loading', "Loading document symbols for '{0}'...", posix.basename(textModel.uri.path))), + 100 + ); + } + + let model = await OutlinePanel._createOutlineModel(textModel, this._editorDisposables); + dispose(loadingMessage); if (!model) { return; } + if (TreeElement.empty(model)) { + return this._showMessage(localize('no-symbols', "No symbols found in document '{0}'", posix.basename(textModel.uri.path))); + } + let newSize = TreeElement.size(model); if (newSize > 7500) { // this is a workaround for performance issues with the tree: https://github.com/Microsoft/vscode/issues/18180 return this._showMessage(localize('too-many-symbols', "We are sorry, but this file is too large for showing an outline.")); } - this._progressBar.stop().hide(); dom.removeClass(this._domNode, 'message'); - let oldModel = this._tree.getInput(); if (event && oldModel && textModel.getLineCount() >= 25) { // heuristic: when the symbols-to-lines ratio changes by 50% between edits @@ -425,17 +502,29 @@ export class OutlinePanel extends ViewletPanel { let oldLength = newLength - event.changes.reduce((prev, value) => prev + value.rangeLength, 0); let oldRatio = oldSize / oldLength; if (newRatio <= oldRatio * 0.5 || newRatio >= oldRatio * 1.5) { - if (!await asDisposablePromise( - TPromise.timeout(2000).then(_ => true), - false, - this._editorDisposables).promise - ) { + + let waitPromise = new Promise(resolve => { + let handle = setTimeout(() => { + handle = undefined; + resolve(true); + }, 2000); + this._disposables.push({ + dispose() { + clearTimeout(handle); + resolve(false); + } + }); + }); + + if (!await waitPromise) { return; } } } - if (oldModel && oldModel.adopt(model)) { + this._progressBar.stop().hide(); + + if (oldModel && oldModel.merge(model)) { this._tree.refresh(undefined, true); model = oldModel; @@ -447,19 +536,27 @@ export class OutlinePanel extends ViewletPanel { } await this._tree.setInput(model); let state = this._treeStates.get(model.textModel.uri.toString()); - OutlineTreeState.restore(this._tree, state); + await OutlineTreeState.restore(this._tree, state, this); } this._input.enable(); this.layoutBody(); + // transfer focus from domNode to the tree + if (this._domNode === document.activeElement) { + this._tree.domFocus(); + } + // feature: filter on type // on type -> update filters // on first type -> capture tree state // on erase -> restore captured tree state let beforePatternState: OutlineTreeState; - this._editorDisposables.push(this._input.onDidChange(async pattern => { - if (!beforePatternState) { + let onInputValueChanged = async pattern => { + + this._contextKeyFiltered.set(pattern.length > 0); + + if (pattern && !beforePatternState) { beforePatternState = OutlineTreeState.capture(this._tree); } let item = model.updateMatches(pattern); @@ -472,15 +569,21 @@ export class OutlinePanel extends ViewletPanel { } if (!pattern && beforePatternState) { - await OutlineTreeState.restore(this._tree, beforePatternState); + await OutlineTreeState.restore(this._tree, beforePatternState, this); beforePatternState = undefined; } - })); + }; + if (this._input.value) { + onInputValueChanged(this._input.value); + } + this._editorDisposables.push(this._input.onDidChange(onInputValueChanged)); + + this._editorDisposables.push(toDisposable(() => this._contextKeyFiltered.reset())); // feature: reveal outline selection in editor // on change -> reveal/select defining range this._editorDisposables.push(this._tree.onDidChangeSelection(e => { - if (e.payload === this) { + if (e.payload === this || e.payload && e.payload.didClickOnTwistie) { return; } let [first] = e.selection; @@ -500,12 +603,20 @@ export class OutlinePanel extends ViewletPanel { aside = !this._tree.useAltAsMultipleSelectionModifier && event.altKey || this._tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey); } } - this._revealTreeSelection(first, focus, aside); + this._revealTreeSelection(model, first, focus, aside); })); // feature: reveal editor selection in outline - this._editorDisposables.push(editor.onDidChangeCursorSelection(e => e.reason === CursorChangeReason.Explicit && this._revealEditorSelection(model, e.selection))); this._revealEditorSelection(model, editor.getSelection()); + const versionIdThen = model.textModel.getVersionId(); + this._editorDisposables.push(editor.onDidChangeCursorSelection(e => { + // first check if the document has changed and stop revealing the + // cursor position iff it has -> we will update/recompute the + // outline view then anyways + if (!model.textModel.isDisposed() && model.textModel.getVersionId() === versionIdThen) { + this._revealEditorSelection(model, e.selection); + } + })); // feature: show markers in outline const updateMarker = (e: URI[], ignoreEmpty?: boolean) => { @@ -526,6 +637,8 @@ export class OutlinePanel extends ViewletPanel { this._editorDisposables.push(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(OutlineConfigKeys.problemsBadges) || e.affectsConfiguration(OutlineConfigKeys.problemsColors)) { + this._treeRenderer.renderProblemColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); + this._treeRenderer.renderProblemBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); this._tree.refresh(undefined, true); return; } @@ -541,27 +654,90 @@ export class OutlinePanel extends ViewletPanel { })); } - private async _revealTreeSelection(element: OutlineElement, focus: boolean, aside: boolean): TPromise { - let { range, uri } = element.symbol.location; - let input = this._editorService.createInput({ resource: uri }); - await this._editorService.openEditor(input, { preserveFocus: !focus, selection: Range.collapseToStart(range), revealInCenterIfOutsideViewport: true, forceOpen: true }, aside ? SIDE_GROUP : ACTIVE_GROUP); + private _applyTypeToFilter(): void { + // depending on the user setting we filter or find elements + if (this._outlineViewState.filterOnType) { + this._treeFilter.enabled = true; + this._treeDataSource.filterOnScore = true; + this._input.setPlaceHolder(localize('filter', "Filter")); + } else { + this._treeFilter.enabled = false; + this._treeDataSource.filterOnScore = false; + this._input.setPlaceHolder(localize('find', "Find")); + } + if (this._tree.getInput()) { + this._tree.refresh(undefined, true); + } } - private async _revealEditorSelection(model: OutlineModel, selection: Selection): TPromise { - if (!this._outlineViewState.followCursor) { + private async _revealTreeSelection(model: OutlineModel, element: OutlineElement, focus: boolean, aside: boolean): Promise { + + await this._editorService.openEditor({ + resource: model.textModel.uri, + options: { + preserveFocus: !focus, + selection: Range.collapseToStart(element.symbol.selectionRange), + revealInCenterIfOutsideViewport: true + } + } as IResourceInput, aside ? SIDE_GROUP : ACTIVE_GROUP); + } + + private async _revealEditorSelection(model: OutlineModel, selection: Selection): Promise { + if (!this._outlineViewState.followCursor || !this._tree.getInput() || !selection) { return; } + let [first] = this._tree.getSelection(); let item = model.getItemEnclosingPosition({ lineNumber: selection.selectionStartLineNumber, column: selection.selectionStartColumn - }); + }, first instanceof OutlineElement ? first : undefined); if (item) { await this._tree.reveal(item, .5); this._tree.setFocus(item, this); this._tree.setSelection([item], this); - } else { - this._tree.setSelection([], this); + } + } + + focusHighlightedElement(up: boolean): void { + if (!this._tree.getInput()) { + return; + } + if (!this._tree.isDOMFocused()) { + this._tree.domFocus(); + return; + } + let navi = this._tree.getNavigator(this._tree.getFocus(), false); + let candidate: any; + while (candidate = up ? navi.previous() : navi.next()) { + if (candidate instanceof OutlineElement && candidate.score && candidate.score[1].length > 0) { + this._tree.setFocus(candidate, this); + this._tree.reveal(candidate).then(undefined, onUnexpectedError); + break; + } } } } +async function goUpOrDownToHighligthedElement(accessor: ServicesAccessor, prev: boolean) { + const viewsService = accessor.get(IViewsService); + const view = await viewsService.openView(OutlineViewId); + if (view instanceof OutlinePanel) { + view.focusHighlightedElement(prev); + } +} + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'outline.focusDownHighlighted', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.DownArrow, + when: ContextKeyExpr.and(OutlineViewFiltered, OutlineViewFocused), + handler: accessor => goUpOrDownToHighligthedElement(accessor, false) +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'outline.focusUpHighlighted', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.UpArrow, + when: ContextKeyExpr.and(OutlineViewFiltered, OutlineViewFocused), + handler: accessor => goUpOrDownToHighligthedElement(accessor, true) +}); diff --git a/src/vs/workbench/parts/outline/electron-browser/outlineTree.ts b/src/vs/workbench/parts/outline/electron-browser/outlineTree.ts deleted file mode 100644 index b0f473c4feb..00000000000 --- a/src/vs/workbench/parts/outline/electron-browser/outlineTree.ts +++ /dev/null @@ -1,313 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as dom from 'vs/base/browser/dom'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { values } from 'vs/base/common/collections'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { createMatches } from 'vs/base/common/filters'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; -import 'vs/css!./media/symbol-icons'; -import { Range } from 'vs/editor/common/core/range'; -import { symbolKindToCssClass } from 'vs/editor/common/modes'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from './outlineModel'; -import { getPathLabel } from 'vs/base/common/labels'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { localize } from 'vs/nls'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { OutlineConfigKeys } from 'vs/workbench/parts/outline/electron-browser/outline'; - -export enum OutlineItemCompareType { - ByPosition, - ByName, - ByKind -} - -export class OutlineItemComparator implements ISorter { - - constructor( - public type: OutlineItemCompareType = OutlineItemCompareType.ByPosition - ) { } - - compare(tree: ITree, a: OutlineGroup | OutlineElement, b: OutlineGroup | OutlineElement): number { - - if (a instanceof OutlineGroup && b instanceof OutlineGroup) { - return a.providerIndex - b.providerIndex; - } - - if (a instanceof OutlineElement && b instanceof OutlineElement) { - switch (this.type) { - case OutlineItemCompareType.ByKind: - return a.symbol.kind - b.symbol.kind; - case OutlineItemCompareType.ByName: - return a.symbol.name.localeCompare(b.symbol.name); - case OutlineItemCompareType.ByPosition: - default: - return Range.compareRangesUsingStarts(a.symbol.location.range, b.symbol.location.range); - } - } - - return 0; - } -} - -export class OutlineItemFilter implements IFilter { - - isVisible(tree: ITree, element: OutlineElement | any): boolean { - return !(element instanceof OutlineElement) || Boolean(element.score); - } -} - -export class OutlineDataSource implements IDataSource { - - getId(tree: ITree, element: TreeElement): string { - return element.id; - } - - hasChildren(tree: ITree, element: OutlineModel | OutlineGroup | OutlineElement): boolean { - if (element instanceof OutlineModel) { - return true; - } - if (element instanceof OutlineElement && !element.score) { - return false; - } - for (const id in element.children) { - if (element.children[id].score) { - return true; - } - } - return false; - } - - async getChildren(tree: ITree, element: TreeElement): TPromise { - let res = values(element.children); - // console.log(element.id + ' with children ' + res.length); - return res; - } - - async getParent(tree: ITree, element: TreeElement | any): TPromise { - return element.parent; - } - - shouldAutoexpand(tree: ITree, element: TreeElement): boolean { - return element instanceof OutlineModel || element.parent instanceof OutlineModel || element instanceof OutlineGroup || element.parent instanceof OutlineGroup; - } -} - -export interface OutlineTemplate { - labelContainer: HTMLElement; - label: HighlightedLabel; - icon?: HTMLElement; - decoration?: HTMLElement; -} - -export class OutlineRenderer implements IRenderer { - - constructor( - @IExtensionService readonly _extensionService: IExtensionService, - @IEnvironmentService readonly _environmentService: IEnvironmentService, - @IWorkspaceContextService readonly _contextService: IWorkspaceContextService, - @IThemeService readonly _themeService: IThemeService, - @IConfigurationService readonly _configurationService: IConfigurationService - ) { - // - } - - getHeight(tree: ITree, element: any): number { - return 22; - } - - getTemplateId(tree: ITree, element: OutlineGroup | OutlineElement): string { - return element instanceof OutlineGroup ? 'outline-group' : 'outline-element'; - } - - renderTemplate(tree: ITree, templateId: string, container: HTMLElement): OutlineTemplate { - if (templateId === 'outline-element') { - const icon = dom.$('.outline-element-icon symbol-icon'); - const labelContainer = dom.$('.outline-element-label'); - const decoration = dom.$('.outline-element-decoration'); - dom.addClass(container, 'outline-element'); - dom.append(container, icon, labelContainer, decoration); - return { icon, labelContainer, label: new HighlightedLabel(labelContainer), decoration }; - } - if (templateId === 'outline-group') { - const labelContainer = dom.$('.outline-element-label'); - dom.addClass(container, 'outline-element'); - dom.append(container, labelContainer); - return { labelContainer, label: new HighlightedLabel(labelContainer) }; - } - - throw new Error(templateId); - } - - renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { - if (element instanceof OutlineElement) { - template.icon.className = `outline-element-icon symbol-icon ${symbolKindToCssClass(element.symbol.kind)}`; - template.label.set(element.symbol.name, element.score ? createMatches(element.score[1]) : undefined, localize('outline.title', "line {0} in {1}", element.symbol.location.range.startLineNumber, getPathLabel(element.symbol.location.uri, this._contextService, this._environmentService))); - this._renderMarkerInfo(element, template); - - } - if (element instanceof OutlineGroup) { - this._extensionService.getExtensions().then(all => { - let found = false; - for (let i = 0; !found && i < all.length; i++) { - const extension = all[i]; - if (extension.id === element.provider.extensionId) { - template.label.set(extension.displayName); - break; - } - } - }, _err => { - template.label.set(element.provider.extensionId); - }); - } - } - - private _renderMarkerInfo(element: OutlineElement, template: OutlineTemplate): void { - - if (!element.marker) { - dom.hide(template.decoration); - template.labelContainer.style.removeProperty('--outline-element-color'); - return; - } - - const { count, topSev } = element.marker; - const color = this._themeService.getTheme().getColor(topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground).toString(); - - // color of the label - if (this._configurationService.getValue(OutlineConfigKeys.problemsColors)) { - template.labelContainer.style.setProperty('--outline-element-color', color); - } else { - template.labelContainer.style.removeProperty('--outline-element-color'); - } - - // badge with color/rollup - if (!this._configurationService.getValue(OutlineConfigKeys.problemsBadges)) { - dom.hide(template.decoration); - - } else if (count > 0) { - dom.show(template.decoration); - dom.removeClass(template.decoration, 'bubble'); - template.decoration.innerText = count < 10 ? count.toString() : '+9'; - template.decoration.title = count === 1 ? localize('1.problem', "1 problem in this element") : localize('N.problem', "{0} problems in this element", count); - template.decoration.style.setProperty('--outline-element-color', color); - - } else { - dom.show(template.decoration); - dom.addClass(template.decoration, 'bubble'); - template.decoration.innerText = '\uf052'; - template.decoration.title = localize('deep.problem', "Contains elements with problems"); - template.decoration.style.setProperty('--outline-element-color', color); - } - } - - disposeTemplate(tree: ITree, templateId: string, template: OutlineTemplate): void { - template.label.dispose(); - } - -} - -export class OutlineTreeState { - - readonly selected: string; - readonly focused: string; - readonly expanded: string[]; - - static capture(tree: ITree): OutlineTreeState { - // selection - let selected: string; - let element = tree.getSelection()[0]; - if (element instanceof TreeElement) { - selected = element.id; - } - - // focus - let focused: string; - element = tree.getFocus(true); - if (element instanceof TreeElement) { - focused = element.id; - } - - // expansion - let expanded = new Array(); - let nav = tree.getNavigator(); - while (nav.next()) { - let element = nav.current(); - if (element instanceof TreeElement) { - if (tree.isExpanded(element)) { - expanded.push(element.id); - } - } - } - return { selected, focused, expanded }; - } - - static async restore(tree: ITree, state: OutlineTreeState): TPromise { - let model = tree.getInput(); - if (!state || !(model instanceof OutlineModel)) { - return TPromise.as(undefined); - } - - // expansion - let items: TreeElement[] = []; - for (const id of state.expanded) { - let item = model.getItemById(id); - if (item) { - items.push(item); - } - } - await tree.collapseAll(undefined); - await tree.expandAll(items); - - // selection & focus - let selected = model.getItemById(state.selected); - let focused = model.getItemById(state.focused); - tree.setSelection([selected]); - tree.setFocus(focused); - } -} - -export class OutlineController extends WorkbenchTreeController { - - protected onLeftClick(tree: ITree, element: any, event: IMouseEvent, origin: string = 'mouse'): boolean { - - const payload = { origin: origin, originalEvent: event }; - - if (tree.getInput() === element) { - tree.clearFocus(payload); - tree.clearSelection(payload); - } else { - const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; - if (!isMouseDown) { - event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise - } - event.stopPropagation(); - - tree.domFocus(); - tree.setSelection([element], payload); - tree.setFocus(element, payload); - - const didClickElement = element instanceof OutlineElement && !this.isClickOnTwistie(event); - - if (!didClickElement) { - if (tree.isExpanded(element)) { - tree.collapse(element).then(null, onUnexpectedError); - } else { - tree.expand(element).then(null, onUnexpectedError); - } - } - } - return true; - } -} diff --git a/src/vs/workbench/parts/outline/test/electron-browser/outlineModel.test.ts b/src/vs/workbench/parts/outline/test/electron-browser/outlineModel.test.ts deleted file mode 100644 index fa61b3523eb..00000000000 --- a/src/vs/workbench/parts/outline/test/electron-browser/outlineModel.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import { OutlineElement, OutlineGroup } from 'vs/workbench/parts/outline/electron-browser/outlineModel'; -import { SymbolKind, SymbolInformation } from 'vs/editor/common/modes'; -import { Range } from 'vs/editor/common/core/range'; -import URI from 'vs/base/common/uri'; -import { IMarker } from 'vs/platform/markers/common/markers'; - -suite('OutlineModel', function () { - - - function fakeSymbolInformation(range: Range, name: string = 'foo'): SymbolInformation { - return { - name, - kind: SymbolKind.Boolean, - location: { uri: URI.parse('some:uri'), range }, - definingRange: range - }; - } - - function fakeMarker(range: Range): IMarker { - return { ...range, owner: 'ffff', message: 'test', severity: 0, resource: null }; - } - - test.skip('OutlineElement - updateMarker', function () { - - let e0 = new OutlineElement('foo1', null, fakeSymbolInformation(new Range(1, 1, 1, 10))); - let e1 = new OutlineElement('foo2', null, fakeSymbolInformation(new Range(2, 1, 5, 1))); - let e2 = new OutlineElement('foo3', null, fakeSymbolInformation(new Range(6, 1, 10, 10))); - - let group = new OutlineGroup('group', null, null, 1); - group.children[e0.id] = e0; - group.children[e1.id] = e1; - group.children[e2.id] = e2; - - const data = [fakeMarker(new Range(6, 1, 6, 7)), fakeMarker(new Range(1, 1, 1, 4)), fakeMarker(new Range(10, 2, 14, 1))]; - data.sort(Range.compareRangesUsingStarts); // model does this - - group.updateMarker(data); - assert.equal(e0.marker.count, 1); - assert.equal(e1.marker.count, 0); - assert.equal(e2.marker.count, 2); - - group.updateMarker([]); - assert.equal(e0.marker.count, 0); - assert.equal(e1.marker.count, 0); - assert.equal(e2.marker.count, 0); - }); - -}); diff --git a/src/vs/workbench/parts/output/browser/logViewer.ts b/src/vs/workbench/parts/output/browser/logViewer.ts index 69588a3fd24..12786761194 100644 --- a/src/vs/workbench/parts/output/browser/logViewer.ts +++ b/src/vs/workbench/parts/output/browser/logViewer.ts @@ -14,22 +14,23 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; -import { LOG_SCHEME } from 'vs/workbench/parts/output/common/output'; +import { LOG_SCHEME, IOutputChannelDescriptor } from 'vs/workbench/parts/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWindowService } from 'vs/platform/windows/common/windows'; export class LogViewerInput extends ResourceEditorInput { public static readonly ID = 'workbench.editorinputs.output'; - constructor(private file: URI, + constructor(private outputChannelDescriptor: IOutputChannelDescriptor, @ITextModelService textModelResolverService: ITextModelService, @IHashService hashService: IHashService ) { - super(paths.basename(file.fsPath), paths.dirname(file.fsPath), file.with({ scheme: LOG_SCHEME }), textModelResolverService, hashService); + super(paths.basename(outputChannelDescriptor.file.path), paths.dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), textModelResolverService, hashService); } public getTypeId(): string { @@ -37,7 +38,7 @@ export class LogViewerInput extends ResourceEditorInput { } public getResource(): URI { - return this.file; + return this.outputChannelDescriptor.file; } } @@ -54,9 +55,10 @@ export class LogViewer extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IWindowService windowService: IWindowService ) { - super(LogViewer.LOG_VIEWER_EDITOR_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, textFileService, editorService); + super(LogViewer.LOG_VIEWER_EDITOR_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, textFileService, editorService, windowService); } protected getConfigurationOverrides(): IEditorOptions { diff --git a/src/vs/workbench/parts/output/browser/outputActions.ts b/src/vs/workbench/parts/output/browser/outputActions.ts index c73e611e656..8d4a20084d7 100644 --- a/src/vs/workbench/parts/output/browser/outputActions.ts +++ b/src/vs/workbench/parts/output/browser/outputActions.ts @@ -6,8 +6,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import * as aria from 'vs/base/browser/ui/aria/aria'; import { IAction, Action } from 'vs/base/common/actions'; -import { IOutputService, OUTPUT_PANEL_ID, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannelIdentifier, COMMAND_OPEN_LOG_VIEWER } from 'vs/workbench/parts/output/common/output'; +import { IOutputService, OUTPUT_PANEL_ID, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannelDescriptor } from 'vs/workbench/parts/output/common/output'; import { SelectActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; @@ -18,8 +19,10 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { Registry } from 'vs/platform/registry/common/platform'; import { groupBy } from 'vs/base/common/arrays'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import URI from 'vs/base/common/uri'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LogViewerInput } from 'vs/workbench/parts/output/browser/logViewer'; export class ToggleOutputAction extends TogglePanelAction { @@ -49,6 +52,7 @@ export class ClearOutputAction extends Action { public run(): TPromise { this.outputService.getActiveChannel().clear(); + aria.status(nls.localize('outputCleared', "Output was cleared")); return TPromise.as(true); } @@ -110,13 +114,16 @@ export class SwitchOutputActionItem extends SelectActionItem { private static readonly SEPARATOR = '─────────'; + private outputChannels: IOutputChannelDescriptor[]; + private logChannels: IOutputChannelDescriptor[]; + constructor( action: IAction, @IOutputService private outputService: IOutputService, @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, [], 0, contextViewService); + super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputs', 'Outputs') }); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); this.toDispose.push(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions(this.outputService.getActiveChannel().id))); @@ -127,31 +134,31 @@ export class SwitchOutputActionItem extends SelectActionItem { this.updateOtions(this.outputService.getActiveChannel().id); } - protected getActionContext(option: string): string { - const channel = this.outputService.getChannels().filter(channelData => channelData.label === option).pop(); + protected getActionContext(option: string, index: number): string { + const channel = index < this.outputChannels.length ? this.outputChannels[index] : this.logChannels[index - this.outputChannels.length - 1]; return channel ? channel.id : option; } private updateOtions(selectedChannel: string): void { - const groups = groupBy(this.outputService.getChannels(), (c1: IOutputChannelIdentifier, c2: IOutputChannelIdentifier) => { - if (!c1.file && c2.file) { + const groups = groupBy(this.outputService.getChannelDescriptors(), (c1: IOutputChannelDescriptor, c2: IOutputChannelDescriptor) => { + if (!c1.log && c2.log) { return -1; } - if (c1.file && !c2.file) { + if (c1.log && !c2.log) { return 1; } return 0; }); - const channels = groups[0] || []; - const fileChannels = groups[1] || []; - const showSeparator = channels.length && fileChannels.length; - const separatorIndex = showSeparator ? channels.length : -1; - const options: string[] = [...channels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionItem.SEPARATOR] : []), ...fileChannels.map(c => c.label)]; + this.outputChannels = groups[0] || []; + this.logChannels = groups[1] || []; + const showSeparator = this.outputChannels.length && this.logChannels.length; + const separatorIndex = showSeparator ? this.outputChannels.length : -1; + const options: string[] = [...this.outputChannels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionItem.SEPARATOR] : []), ...this.logChannels.map(c => nls.localize('logChannel', "Log ({0})", c.label))]; let selected = 0; if (selectedChannel) { - selected = channels.map(c => c.id).indexOf(selectedChannel); + selected = this.outputChannels.map(c => c.id).indexOf(selectedChannel); if (selected === -1) { - selected = separatorIndex + 1 + fileChannels.map(c => c.id).indexOf(selectedChannel); + selected = separatorIndex + 1 + this.logChannels.map(c => c.id).indexOf(selectedChannel); } } this.setOptions(options, Math.max(0, selected), separatorIndex !== -1 ? separatorIndex : void 0); @@ -166,8 +173,9 @@ export class OpenLogOutputFile extends Action { private disposables: IDisposable[] = []; constructor( - @ICommandService private commandService: ICommandService, - @IOutputService private outputService: IOutputService + @IOutputService private outputService: IOutputService, + @IEditorService private editorService: IEditorService, + @IInstantiationService private instantiationService: IInstantiationService ) { super(OpenLogOutputFile.ID, OpenLogOutputFile.LABEL, 'output-action open-log-file'); this.outputService.onActiveOutputChannel(this.update, this, this.disposables); @@ -175,17 +183,74 @@ export class OpenLogOutputFile extends Action { } private update(): void { - const logFile = this.getActiveLogChannelFile(); - this.enabled = !!logFile; + const outputChannelDescriptor = this.getOutputChannelDescriptor(); + this.enabled = outputChannelDescriptor && outputChannelDescriptor.file && outputChannelDescriptor.log; } public run(): TPromise { - return this.commandService.executeCommand(COMMAND_OPEN_LOG_VIEWER, this.getActiveLogChannelFile()); + return this.enabled ? this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, this.getOutputChannelDescriptor())).then(() => null) : TPromise.as(null); } - private getActiveLogChannelFile(): URI { + private getOutputChannelDescriptor(): IOutputChannelDescriptor { const channel = this.outputService.getActiveChannel(); - const identifier = channel ? this.outputService.getChannels().filter(c => c.id === channel.id)[0] : null; - return identifier ? identifier.file : null; + return channel ? this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0] : null; } } + +export class ShowLogsOutputChannelAction extends Action { + + static ID = 'workbench.action.showLogs'; + static LABEL = nls.localize('showLogs', "Show Logs..."); + + constructor(id: string, label: string, + @IQuickInputService private quickInputService: IQuickInputService, + @IOutputService private outputService: IOutputService + ) { + super(id, label); + } + + run(): TPromise { + const entries: IQuickPickItem[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) + .map(({ id, label }) => ({ id, label })); + + return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log") }) + .then(entry => { + if (entry) { + return this.outputService.showChannel(entry.id); + } + return null; + }); + } +} + +interface IOutputChannelQuickPickItem extends IQuickPickItem { + channel: IOutputChannelDescriptor; +} + +export class OpenOutputLogFileAction extends Action { + + static ID = 'workbench.action.openLogFile'; + static LABEL = nls.localize('openLogFile', "Open Log File..."); + + constructor(id: string, label: string, + @IQuickInputService private quickInputService: IQuickInputService, + @IOutputService private outputService: IOutputService, + @IEditorService private editorService: IEditorService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(id, label); + } + + run(): TPromise { + const entries: IOutputChannelQuickPickItem[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) + .map(channel => ({ id: channel.id, label: channel.label, channel })); + + return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log file") }) + .then(entry => { + if (entry) { + return this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, entry.channel)).then(() => null); + } + return null; + }); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/output/browser/outputPanel.ts b/src/vs/workbench/parts/output/browser/outputPanel.ts index a9151ee685c..4753c5b88f0 100644 --- a/src/vs/workbench/parts/output/browser/outputPanel.ts +++ b/src/vs/workbench/parts/output/browser/outputPanel.ts @@ -25,6 +25,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWindowService } from 'vs/platform/windows/common/windows'; export class OutputPanel extends AbstractTextResourceEditor { private actions: IAction[]; @@ -41,9 +42,10 @@ export class OutputPanel extends AbstractTextResourceEditor { @IContextKeyService private contextKeyService: IContextKeyService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService textFileService: ITextFileService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IWindowService windowService: IWindowService ) { - super(OUTPUT_PANEL_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, textFileService, editorService); + super(OUTPUT_PANEL_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, textFileService, editorService, windowService); this.scopedInstantiationService = instantiationService; } @@ -65,9 +67,7 @@ export class OutputPanel extends AbstractTextResourceEditor { this.instantiationService.createInstance(OpenLogOutputFile) ]; - this.actions.forEach(a => { - this.toUnbind.push(a); - }); + this.actions.forEach(a => this._register(a)); } return this.actions; @@ -134,8 +134,7 @@ export class OutputPanel extends AbstractTextResourceEditor { protected createEditor(parent: HTMLElement): void { // First create the scoped instantation service and only then construct the editor using the scoped service - const scopedContextKeyService = this.contextKeyService.createScoped(parent); - this.toUnbind.push(scopedContextKeyService); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); super.createEditor(parent); diff --git a/src/vs/workbench/parts/output/common/output.ts b/src/vs/workbench/parts/output/common/output.ts index e28182005cf..b4f12cb355c 100644 --- a/src/vs/workbench/parts/output/common/output.ts +++ b/src/vs/workbench/parts/output/common/output.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; /** * Mime type used by the output editor. @@ -46,11 +46,6 @@ export const LOG_MODE_ID = 'log'; */ export const OUTPUT_PANEL_ID = 'workbench.panel.output'; -/** - * Open log viewer command id - */ -export const COMMAND_OPEN_LOG_VIEWER = 'workbench.action.openLogViewer'; - export const Extensions = { OutputChannels: 'workbench.contributions.outputChannels' }; @@ -78,9 +73,9 @@ export interface IOutputService { getChannel(id: string): IOutputChannel; /** - * Returns an array of all known output channels as identifiers. + * Returns an array of all known output channels descriptors. */ - getChannels(): IOutputChannelIdentifier[]; + getChannelDescriptors(): IOutputChannelDescriptor[]; /** * Returns the currently active channel. @@ -132,9 +127,10 @@ export interface IOutputChannel { dispose(): void; } -export interface IOutputChannelIdentifier { +export interface IOutputChannelDescriptor { id: string; label: string; + log: boolean; file?: URI; } @@ -146,17 +142,17 @@ export interface IOutputChannelRegistry { /** * Make an output channel known to the output world. */ - registerChannel(id: string, name: string, file?: URI): void; + registerChannel(descriptor: IOutputChannelDescriptor): void; /** * Returns the list of channels known to the output world. */ - getChannels(): IOutputChannelIdentifier[]; + getChannels(): IOutputChannelDescriptor[]; /** * Returns the channel with the passed id. */ - getChannel(id: string): IOutputChannelIdentifier; + getChannel(id: string): IOutputChannelDescriptor; /** * Remove the output channel with the passed id. @@ -165,7 +161,7 @@ export interface IOutputChannelRegistry { } class OutputChannelRegistry implements IOutputChannelRegistry { - private channels = new Map(); + private channels = new Map(); private readonly _onDidRegisterChannel: Emitter = new Emitter(); readonly onDidRegisterChannel: Event = this._onDidRegisterChannel.event; @@ -173,20 +169,20 @@ class OutputChannelRegistry implements IOutputChannelRegistry { private readonly _onDidRemoveChannel: Emitter = new Emitter(); readonly onDidRemoveChannel: Event = this._onDidRemoveChannel.event; - public registerChannel(id: string, label: string, file?: URI): void { - if (!this.channels.has(id)) { - this.channels.set(id, { id, label, file }); - this._onDidRegisterChannel.fire(id); + public registerChannel(descriptor: IOutputChannelDescriptor): void { + if (!this.channels.has(descriptor.id)) { + this.channels.set(descriptor.id, descriptor); + this._onDidRegisterChannel.fire(descriptor.id); } } - public getChannels(): IOutputChannelIdentifier[] { - const result: IOutputChannelIdentifier[] = []; + public getChannels(): IOutputChannelDescriptor[] { + const result: IOutputChannelDescriptor[] = []; this.channels.forEach(value => result.push(value)); return result; } - public getChannel(id: string): IOutputChannelIdentifier { + public getChannel(id: string): IOutputChannelDescriptor { return this.channels.get(id); } diff --git a/src/vs/workbench/parts/output/common/outputLinkComputer.ts b/src/vs/workbench/parts/output/common/outputLinkComputer.ts index 54334478c76..b6f7c700a8b 100644 --- a/src/vs/workbench/parts/output/common/outputLinkComputer.ts +++ b/src/vs/workbench/parts/output/common/outputLinkComputer.ts @@ -6,9 +6,9 @@ import { IMirrorModel, IWorkerContext } from 'vs/editor/common/services/editorSimpleWorker'; import { ILink } from 'vs/editor/common/modes'; -import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import * as strings from 'vs/base/common/strings'; import * as arrays from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; @@ -56,7 +56,7 @@ export class OutputLinkComputer { return null; } - public computeLinks(uri: string): TPromise { + public computeLinks(uri: string): ILink[] { const model = this.getModel(uri); if (!model) { return void 0; @@ -70,7 +70,7 @@ export class OutputLinkComputer { const resourceCreator: IResourceCreator = { toResource: (folderRelativePath: string): URI => { if (typeof folderRelativePath === 'string') { - return folderUri.with({ path: paths.join(folderUri.path, folderRelativePath) }); + return resources.joinPath(folderUri, folderRelativePath); } return null; @@ -82,7 +82,7 @@ export class OutputLinkComputer { } }); - return TPromise.as(links); + return links; } public static createPatterns(workspaceFolder: URI): RegExp[] { diff --git a/src/vs/workbench/parts/output/common/outputLinkProvider.ts b/src/vs/workbench/parts/output/common/outputLinkProvider.ts index 8014d8bd950..54d7402f8ac 100644 --- a/src/vs/workbench/parts/output/common/outputLinkProvider.ts +++ b/src/vs/workbench/parts/output/common/outputLinkProvider.ts @@ -6,8 +6,8 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; -import { RunOnceScheduler, wireCancellationToken } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; import { LinkProviderRegistry, ILink } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -46,7 +46,7 @@ export class OutputLinkProvider { if (!this.linkProviderRegistration) { this.linkProviderRegistration = LinkProviderRegistry.register([{ language: OUTPUT_MODE_ID, scheme: '*' }, { language: LOG_MODE_ID, scheme: '*' }], { provideLinks: (model, token): Thenable => { - return wireCancellationToken(token, this.provideLinks(model.uri)); + return this.provideLinks(model.uri); } }); } diff --git a/src/vs/workbench/parts/output/electron-browser/output.contribution.ts b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts index 63be0b0f1c2..2b873f3b1c1 100644 --- a/src/vs/workbench/parts/output/electron-browser/output.contribution.ts +++ b/src/vs/workbench/parts/output/electron-browser/output.contribution.ts @@ -12,8 +12,8 @@ import { KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { OutputService, LogContentProvider } from 'vs/workbench/parts/output/electron-browser/outputServices'; -import { ToggleOutputAction, ClearOutputAction, OpenLogOutputFile } from 'vs/workbench/parts/output/browser/outputActions'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_PANEL_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, COMMAND_OPEN_LOG_VIEWER, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; +import { ToggleOutputAction, ClearOutputAction, OpenLogOutputFile, ShowLogsOutputChannelAction, OpenOutputLogFileAction } from 'vs/workbench/parts/output/browser/outputActions'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_PANEL_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; import { PanelRegistry, Extensions, PanelDescriptor } from 'vs/workbench/browser/panel'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -23,10 +23,8 @@ import { LogViewer, LogViewerInput } from 'vs/workbench/parts/output/browser/log import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import URI from 'vs/base/common/uri'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; // Register Service registerSingleton(IOutputService, OutputService); @@ -91,6 +89,9 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleOutputActi actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearOutputAction, ClearOutputAction.ID, ClearOutputAction.LABEL), 'View: Clear Output', nls.localize('viewCategory', "View")); +const devCategory = nls.localize('developer', "Developer"); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowLogsOutputChannelAction, ShowLogsOutputChannelAction.ID, ShowLogsOutputChannelAction.LABEL), 'Developer: Show Logs...', devCategory); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenOutputLogFileAction, OpenOutputLogFileAction.ID, OpenOutputLogFileAction.LABEL), 'Developer: Open Log File...', devCategory); interface IActionDescriptor { id: string; @@ -180,10 +181,11 @@ registerAction({ } }); -CommandsRegistry.registerCommand(COMMAND_OPEN_LOG_VIEWER, function (accessor: ServicesAccessor, file: URI) { - if (file) { - const editorService = accessor.get(IEditorService); - return editorService.openEditor(accessor.get(IInstantiationService).createInstance(LogViewerInput, file)); - } - return null; +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: ToggleOutputAction.ID, + title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") + }, + order: 1 }); diff --git a/src/vs/workbench/parts/output/electron-browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts index 32fc13e7979..4cc69a0e442 100644 --- a/src/vs/workbench/parts/output/electron-browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -7,17 +7,16 @@ import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; import * as strings from 'vs/base/common/strings'; import * as extfs from 'vs/base/node/extfs'; -import * as fs from 'fs'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, MAX_OUTPUT_LENGTH, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output'; +import { IOutputChannelDescriptor, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, MAX_OUTPUT_LENGTH } from 'vs/workbench/parts/output/common/output'; import { OutputPanel } from 'vs/workbench/parts/output/browser/outputPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -33,15 +32,15 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IPanel } from 'vs/workbench/common/panel'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { RotatingLogger } from 'spdlog'; import { toLocalISOString } from 'vs/base/common/date'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILogService } from 'vs/platform/log/common/log'; import { binarySearch } from 'vs/base/common/arrays'; -import { Schemas } from 'vs/base/common/network'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { OutputAppender } from 'vs/platform/output/node/outputAppender'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -50,7 +49,7 @@ let callbacks: ((eventType: string, fileName: string) => void)[] = []; function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable { callbacks.push(onChange); if (!watchingOutputDir) { - const watcher = extfs.watch(outputDir, (eventType, fileName) => { + const watcherDisposable = extfs.watch(outputDir, (eventType, fileName) => { for (const callback of callbacks) { callback(eventType, fileName); } @@ -60,55 +59,12 @@ function watchOutputDirectory(outputDir: string, logService: ILogService, onChan watchingOutputDir = true; return toDisposable(() => { callbacks = []; - if (watcher) { - watcher.removeAllListeners(); - watcher.close(); - } + watcherDisposable.dispose(); }); } return toDisposable(() => { }); } -const fileWatchers: Map = new Map(); -function watchFile(file: string, callback: () => void): IDisposable { - - const onFileChange = (file: string) => { - for (const callback of fileWatchers.get(file)) { - callback(); - } - }; - - let callbacks = fileWatchers.get(file); - if (!callbacks) { - callbacks = []; - fileWatchers.set(file, callbacks); - fs.watchFile(file, { interval: 1000 }, (current, previous) => { - if ((previous && !current) || (!previous && !current)) { - onFileChange(file); - return; - } - if (previous && current && previous.mtime !== current.mtime) { - onFileChange(file); - return; - } - }); - } - callbacks.push(callback); - return toDisposable(() => { - let allCallbacks = fileWatchers.get(file); - allCallbacks.splice(allCallbacks.indexOf(callback), 1); - if (!allCallbacks.length) { - fs.unwatchFile(file); - fileWatchers.delete(file); - } - }); -} - -function unWatchAllFiles(): void { - fileWatchers.forEach((value, file) => fs.unwatchFile(file)); - fileWatchers.clear(); -} - interface OutputChannel extends IOutputChannel { readonly file: URI; readonly onDidAppendedContent: Event; @@ -126,6 +82,7 @@ abstract class AbstractFileOutputChannel extends Disposable { protected _onDispose: Emitter = new Emitter(); readonly onDispose: Event = this._onDispose.event; + private readonly mimeType: string; protected modelUpdater: RunOnceScheduler; protected model: ITextModel; readonly file: URI; @@ -134,30 +91,31 @@ abstract class AbstractFileOutputChannel extends Disposable { protected endOffset: number = 0; constructor( - protected readonly outputChannelIdentifier: IOutputChannelIdentifier, + readonly outputChannelDescriptor: IOutputChannelDescriptor, private readonly modelUri: URI, - private mimeType: string, protected fileService: IFileService, protected modelService: IModelService, protected modeService: IModeService, ) { super(); - this.file = this.outputChannelIdentifier.file; + this.mimeType = outputChannelDescriptor.log ? LOG_MIME : OUTPUT_MIME; + this.file = this.outputChannelDescriptor.file; this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300); this._register(toDisposable(() => this.modelUpdater.cancel())); } get id(): string { - return this.outputChannelIdentifier.id; + return this.outputChannelDescriptor.id; } get label(): string { - return this.outputChannelIdentifier.label; + return this.outputChannelDescriptor.label; } clear(): void { if (this.modelUpdater.isScheduled()) { this.modelUpdater.cancel(); + this.onUpdateModelCancelled(); } if (this.model) { this.model.setValue(''); @@ -192,6 +150,7 @@ abstract class AbstractFileOutputChannel extends Disposable { protected onModelCreated(model: ITextModel) { } protected onModelWillDispose(model: ITextModel) { } + protected onUpdateModelCancelled() { } protected updateModel() { } dispose(): void { @@ -205,14 +164,14 @@ abstract class AbstractFileOutputChannel extends Disposable { */ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements OutputChannel { - private outputWriter: RotatingLogger; + private appender: OutputAppender; private appendedMessage = ''; private loadingFromFileInProgress: boolean = false; private resettingDelayer: ThrottledDelayer; private readonly rotatingFilePath: string; constructor( - outputChannelIdentifier: IOutputChannelIdentifier, + outputChannelDescriptor: IOutputChannelDescriptor, outputDir: string, modelUri: URI, @IFileService fileService: IFileService, @@ -220,12 +179,11 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements Out @IModeService modeService: IModeService, @ILogService logService: ILogService ) { - super({ ...outputChannelIdentifier, file: URI.file(paths.join(outputDir, `${outputChannelIdentifier.id}.log`)) }, modelUri, OUTPUT_MIME, fileService, modelService, modeService); + super({ ...outputChannelDescriptor, file: URI.file(paths.join(outputDir, `${outputChannelDescriptor.id}.log`)) }, modelUri, fileService, modelService, modeService); // Use one rotating file to check for main file reset - this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, 1024 * 1024 * 30, 1); - this.outputWriter.clearFormatters(); - this.rotatingFilePath = `${outputChannelIdentifier.id}.1.log`; + this.appender = new OutputAppender(this.id, this.file.fsPath); + this.rotatingFilePath = `${outputChannelDescriptor.id}.1.log`; this._register(watchOutputDirectory(paths.dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file))); this.resettingDelayer = new ThrottledDelayer(50); @@ -288,7 +246,7 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements Out } private loadFile(): TPromise { - return this.fileService.resolveContent(this.file, { position: this.startOffset }) + return this.fileService.resolveContent(this.file, { position: this.startOffset, encoding: 'utf8' }) .then(content => this.appendedMessage ? content.value + this.appendedMessage : content.value); } @@ -307,38 +265,57 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements Out } private write(content: string): void { - this.outputWriter.critical(content); + this.appender.append(content); } private flush(): void { - this.outputWriter.flush(); + this.appender.flush(); } } class OutputFileListener extends Disposable { - private readonly _onDidChange: Emitter = new Emitter(); - readonly onDidContentChange: Event = this._onDidChange.event; + private readonly _onDidContentChange: Emitter = new Emitter(); + readonly onDidContentChange: Event = this._onDidContentChange.event; private watching: boolean = false; - private disposables: IDisposable[] = []; + private syncDelayer: ThrottledDelayer; + private etag: string; constructor( private readonly file: URI, + private readonly fileService: IFileService ) { super(); + this.syncDelayer = new ThrottledDelayer(500); } - watch(): void { + watch(eTag: string): void { if (!this.watching) { - this.disposables.push(watchFile(this.file.fsPath, () => this._onDidChange.fire())); + this.etag = eTag; + this.poll(); this.watching = true; } } + private poll(): void { + const loop = () => this.doWatch().then(() => this.poll()); + this.syncDelayer.trigger(loop); + } + + private doWatch(): TPromise { + return this.fileService.resolveFile(this.file) + .then(stat => { + if (stat.etag !== this.etag) { + this.etag = stat.etag; + this._onDidContentChange.fire(stat.size); + } + }); + } + unwatch(): void { if (this.watching) { - this.disposables = dispose(this.disposables); + this.syncDelayer.cancel(); this.watching = false; } } @@ -357,26 +334,27 @@ class FileOutputChannel extends AbstractFileOutputChannel implements OutputChann private readonly fileHandler: OutputFileListener; private updateInProgress: boolean = false; + private etag: string = ''; constructor( - outputChannelIdentifier: IOutputChannelIdentifier, + outputChannelDescriptor: IOutputChannelDescriptor, modelUri: URI, @IFileService fileService: IFileService, @IModelService modelService: IModelService, - @IModeService modeService: IModeService, - @ILogService logService: ILogService, + @IModeService modeService: IModeService ) { - super(outputChannelIdentifier, modelUri, LOG_MIME, fileService, modelService, modeService); + super(outputChannelDescriptor, modelUri, fileService, modelService, modeService); - this.fileHandler = this._register(new OutputFileListener(this.file)); - this._register(this.fileHandler.onDidContentChange(() => this.onDidContentChange())); + this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService)); + this._register(this.fileHandler.onDidContentChange(size => this.onDidContentChange(size))); this._register(toDisposable(() => this.fileHandler.unwatch())); } loadModel(): TPromise { - return this.fileService.resolveContent(this.file, { position: this.startOffset }) + return this.fileService.resolveContent(this.file, { position: this.startOffset, encoding: 'utf8' }) .then(content => { this.endOffset = this.startOffset + Buffer.from(content.value).byteLength; + this.etag = content.etag; return this.createModel(content.value); }); } @@ -387,8 +365,9 @@ class FileOutputChannel extends AbstractFileOutputChannel implements OutputChann protected updateModel(): void { if (this.model) { - this.fileService.resolveContent(this.file, { position: this.endOffset }) + this.fileService.resolveContent(this.file, { position: this.endOffset, encoding: 'utf8' }) .then(content => { + this.etag = content.etag; if (content.value) { this.endOffset = this.endOffset + Buffer.from(content.value).byteLength; this.appendToModel(content.value); @@ -401,16 +380,24 @@ class FileOutputChannel extends AbstractFileOutputChannel implements OutputChann } protected onModelCreated(model: ITextModel): void { - this.fileHandler.watch(); + this.fileHandler.watch(this.etag); } protected onModelWillDispose(model: ITextModel): void { this.fileHandler.unwatch(); } - private onDidContentChange(): void { + protected onUpdateModelCancelled(): void { + this.updateInProgress = false; + } + + private onDidContentChange(size: number): void { if (!this.updateInProgress) { this.updateInProgress = true; + if (this.endOffset > size) { // Reset - Content is removed + this.startOffset = this.endOffset = 0; + this.model.setValue(''); + } this.modelUpdater.schedule(); } } @@ -439,6 +426,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo @IEnvironmentService environmentService: IEnvironmentService, @IWindowService windowService: IWindowService, @ILogService private logService: ILogService, + @ITelemetryService private telemetryService: ITelemetryService, @ILifecycleService private lifecycleService: ILifecycleService, @IContextKeyService private contextKeyService: IContextKeyService, ) { @@ -460,11 +448,9 @@ export class OutputService extends Disposable implements IOutputService, ITextMo panelService.onDidPanelOpen(this.onDidPanelOpen, this); panelService.onDidPanelClose(this.onDidPanelClose, this); - this._register(toDisposable(() => unWatchAllFiles())); - // Set active channel to first channel if not set if (!this.activeChannel) { - const channels = this.getChannels(); + const channels = this.getChannelDescriptors(); this.activeChannel = channels && channels.length > 0 ? this.getChannel(channels[0].id) : null; } @@ -499,7 +485,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.channels.get(id); } - getChannels(): IOutputChannelIdentifier[] { + getChannelDescriptors(): IOutputChannelDescriptor[] { return Registry.as(Extensions.OutputChannels).getChannels(); } @@ -559,7 +545,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo channel.onDispose(() => { Registry.as(Extensions.OutputChannels).removeChannel(id); if (this.activeChannel === channel) { - const channels = this.getChannels(); + const channels = this.getChannelDescriptors(); if (this.isPanelShown() && channels.length) { this.doShowChannel(this.getChannel(channels[0].id), true); this._onActiveOutputChannel.fire(channels[0].id); @@ -589,13 +575,17 @@ export class OutputService extends Disposable implements IOutputService, ITextMo } catch (e) { // Do not crash if spdlog rotating logger cannot be loaded (workaround for https://github.com/Microsoft/vscode/issues/47883) this.logService.error(e); + /* __GDPR__ + "output.channel.creation.error" : {} + */ + this.telemetryService.publicLog('output.channel.creation.error'); return this.instantiationService.createInstance(BufferredOutputChannel, { id, label: channelData ? channelData.label : '' }); } } private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): Thenable { if (this._outputPanel) { - CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(channel instanceof FileOutputChannel); + CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(channel instanceof FileOutputChannel && channel.outputChannelDescriptor.log); return this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: preserveFocus }), CancellationToken.None) .then(() => { if (!preserveFocus) { @@ -635,6 +625,7 @@ export class LogContentProvider { private channels: Map = new Map(); constructor( + @IOutputService private outputService: IOutputService, @IInstantiationService private instantiationService: IInstantiationService ) { } @@ -650,13 +641,16 @@ export class LogContentProvider { } private getChannel(resource: URI): OutputChannel { - const id = resource.path; - let channel = this.channels.get(id); + const channelId = resource.path; + let channel = this.channels.get(channelId); if (!channel) { const channelDisposables: IDisposable[] = []; - channel = this.instantiationService.createInstance(FileOutputChannel, { id, label: '', file: resource.with({ scheme: Schemas.file }) }, resource); - channel.onDispose(() => dispose(channelDisposables), channelDisposables); - this.channels.set(id, channel); + const outputChannelDescriptor = this.outputService.getChannelDescriptors().filter(({ id }) => id === channelId)[0]; + if (outputChannelDescriptor && outputChannelDescriptor.file) { + channel = this.instantiationService.createInstance(FileOutputChannel, outputChannelDescriptor, resource); + channel.onDispose(() => dispose(channelDisposables), channelDisposables); + this.channels.set(channelId, channel); + } } return channel; } @@ -681,7 +675,7 @@ class BufferredOutputChannel extends Disposable implements OutputChannel { private lastReadId: number = void 0; constructor( - protected readonly outputChannelIdentifier: IOutputChannelIdentifier, + protected readonly outputChannelIdentifier: IOutputChannelDescriptor, @IModelService private modelService: IModelService, @IModeService private modeService: IModeService ) { diff --git a/src/vs/workbench/parts/output/test/outputLinkProvider.test.ts b/src/vs/workbench/parts/output/test/outputLinkProvider.test.ts index 7e7f988fbf9..40a8f72192f 100644 --- a/src/vs/workbench/parts/output/test/outputLinkProvider.test.ts +++ b/src/vs/workbench/parts/output/test/outputLinkProvider.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; import { OutputLinkComputer } from 'vs/workbench/parts/output/common/outputLinkComputer'; import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/parts/performance/electron-browser/actions.ts b/src/vs/workbench/parts/performance/electron-browser/actions.ts new file mode 100644 index 00000000000..00ca60d5e6d --- /dev/null +++ b/src/vs/workbench/parts/performance/electron-browser/actions.ts @@ -0,0 +1,307 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Action } from 'vs/base/common/actions'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import * as nls from 'vs/nls'; +import product from 'vs/platform/node/product'; +import pkg from 'vs/platform/node/package'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IIntegrityService } from 'vs/platform/integrity/common/integrity'; +import { ITimerService, IStartupMetrics } from 'vs/workbench/services/timer/electron-browser/timerService'; +import * as os from 'os'; +import { IExtensionService, ActivationTimes } from 'vs/workbench/services/extensions/common/extensions'; +import { getEntries } from 'vs/base/common/performance'; +import { timeout } from 'vs/base/common/async'; +import { StartupKindToString } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { forEach } from 'vs/base/common/collections'; +import { mergeSort } from 'vs/base/common/arrays'; + +class Info { + + static getTimerInfo(metrics: IStartupMetrics, nodeModuleLoadTime?: number): { [name: string]: Info } { + const table: { [name: string]: Info } = Object.create(null); + table['start => app.isReady'] = new Info(metrics.timers.ellapsedAppReady, '[main]', metrics.initialStartup); + table['nls:start => nls:end'] = new Info(metrics.timers.ellapsedNlsGeneration, '[main]', metrics.initialStartup); + table['app.isReady => window.loadUrl()'] = new Info(metrics.timers.ellapsedWindowLoad, '[main]', metrics.initialStartup); + + table['window.loadUrl() => begin to require(workbench.main.js)'] = new Info(metrics.timers.ellapsedWindowLoadToRequire, '[main->renderer]', StartupKindToString(metrics.windowKind)); + table['require(workbench.main.js)'] = new Info(metrics.timers.ellapsedRequire, '[renderer]', `cached data: ${(metrics.didUseCachedData ? 'YES' : 'NO')}${nodeModuleLoadTime ? `, node_modules took ${nodeModuleLoadTime}ms` : ''}`); + + table['register extensions & spawn extension host'] = new Info(metrics.timers.ellapsedExtensions, '[renderer]'); + table['restore viewlet'] = new Info(metrics.timers.ellapsedViewletRestore, '[renderer]', metrics.viewletId); + table['restore panel'] = new Info(metrics.timers.ellapsedPanelRestore, '[renderer]', metrics.panelId); + table['restore editors'] = new Info(metrics.timers.ellapsedEditorRestore, '[renderer]', `${metrics.editorIds.length}: ${metrics.editorIds.join(', ')}`); + table['overall workbench load'] = new Info(metrics.timers.ellapsedWorkbench, '[renderer]'); + + table['workbench ready'] = new Info(metrics.ellapsed, '[main->renderer]'); + table['extensions registered'] = new Info(metrics.timers.ellapsedExtensionsReady, '[renderer]'); + + return table; + } + + private constructor(readonly duration: number, readonly process: string, readonly info: string | boolean = '') { } +} + +class LoaderStat { + + static getLoaderStats() { + + let seq = 1; + const amdLoad = new Map(); + const amdInvoke = new Map(); + const nodeRequire = new Map(); + const nodeEval = new Map(); + + function mark(map: Map, stat: LoaderEvent) { + if (map.has(stat.detail)) { + // console.warn('BAD events, DOUBLE start', stat); + // map.delete(stat.detail); + return; + } + map.set(stat.detail, new LoaderStat(-stat.timestamp, seq++)); + } + + function diff(map: Map, stat: LoaderEvent) { + let obj = map.get(stat.detail); + if (!obj) { + // console.warn('BAD events, end WITHOUT start', stat); + // map.delete(stat.detail); + return; + } + if (obj.duration >= 0) { + // console.warn('BAD events, DOUBLE end', stat); + // map.delete(stat.detail); + return; + } + obj.duration = (obj.duration + stat.timestamp); + } + + const stats = mergeSort(require.getStats().slice(0), (a, b) => a.timestamp - b.timestamp); + + for (const stat of stats) { + switch (stat.type) { + case LoaderEventType.BeginLoadingScript: + mark(amdLoad, stat); + break; + case LoaderEventType.EndLoadingScriptOK: + case LoaderEventType.EndLoadingScriptError: + diff(amdLoad, stat); + break; + + case LoaderEventType.BeginInvokeFactory: + mark(amdInvoke, stat); + break; + case LoaderEventType.EndInvokeFactory: + diff(amdInvoke, stat); + break; + + case LoaderEventType.NodeBeginNativeRequire: + mark(nodeRequire, stat); + break; + case LoaderEventType.NodeEndNativeRequire: + diff(nodeRequire, stat); + break; + + case LoaderEventType.NodeBeginEvaluatingScript: + mark(nodeEval, stat); + break; + case LoaderEventType.NodeEndEvaluatingScript: + diff(nodeEval, stat); + break; + } + } + + function toObject(map: Map): { [name: string]: any } { + const result = Object.create(null); + map.forEach((value, index) => result[index] = value); + return result; + } + + let nodeRequireTotal = 0; + nodeRequire.forEach(value => nodeRequireTotal += value.duration); + + return { + amdLoad: toObject(amdLoad), + amdInvoke: toObject(amdInvoke), + nodeRequire: toObject(nodeRequire), + nodeEval: toObject(nodeEval), + nodeRequireTotal + }; + } + + constructor(public duration: number, public seq: number) { } +} + +export class ShowStartupPerformance extends Action { + + static readonly ID = 'workbench.action.appPerf'; + static readonly LABEL = nls.localize('appPerf', "Startup Performance"); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService, + @ITimerService private timerService: ITimerService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IExtensionService private extensionService: IExtensionService + ) { + super(id, label); + } + + run(): Promise { + + // Show dev tools + this.windowService.openDevTools(); + + Promise.all([ + timeout(1000), // needed to print a table + this.timerService.startupMetrics + ]).then(([, metrics]) => { + + console.group('Startup Performance Measurement'); + console.log(`OS: ${metrics.platform}(${metrics.release})`); + console.log(`CPUs: ${metrics.cpus.model}(${metrics.cpus.count} x ${metrics.cpus.speed})`); + console.log(`Memory(System): ${(metrics.totalmem / (1024 * 1024 * 1024)).toFixed(2)} GB(${(metrics.freemem / (1024 * 1024 * 1024)).toFixed(2)}GB free)`); + console.log(`Memory(Process): ${(metrics.meminfo.workingSetSize / 1024).toFixed(2)} MB working set(${(metrics.meminfo.peakWorkingSetSize / 1024).toFixed(2)}MB peak, ${(metrics.meminfo.privateBytes / 1024).toFixed(2)}MB private, ${(metrics.meminfo.sharedBytes / 1024).toFixed(2)}MB shared)`); + console.log(`VM(likelyhood): ${metrics.isVMLikelyhood}% `); + + console.log(`Initial Startup: ${metrics.initialStartup} `); + console.log(`Has ${metrics.windowCount - 1} other windows`); + console.log(`Screen Reader Active: ${metrics.hasAccessibilitySupport} `); + console.log(`Empty Workspace: ${metrics.emptyWorkbench} `); + + + const loaderStats = this.environmentService.performance && LoaderStat.getLoaderStats(); + + console.table(Info.getTimerInfo(metrics, loaderStats && loaderStats.nodeRequireTotal)); + + if (loaderStats) { + for (const key in loaderStats) { + console.groupCollapsed(`Loader: ${key} `); + console.table(loaderStats[key]); + console.groupEnd(); + } + } + + console.groupEnd(); + + console.group('Extension Activation Stats'); + let extensionsActivationTimes: { [id: string]: ActivationTimes; } = {}; + let extensionsStatus = this.extensionService.getExtensionsStatus(); + for (let id in extensionsStatus) { + const status = extensionsStatus[id]; + if (status.activationTimes) { + extensionsActivationTimes[id] = status.activationTimes; + } + } + console.table(extensionsActivationTimes); + console.groupEnd(); + + console.group('Raw Startup Timers (CSV)'); + let value = `Name\tStart\n`; + let entries = getEntries('mark'); + for (const entry of entries) { + value += `${entry.name} \t${entry.startTime} \n`; + } + console.log(value); + console.groupEnd(); + }); + + return Promise.resolve(true); + } +} + + +// NOTE: This is still used when running --prof-startup, which already opens a dialog, so the reporter is not used. +export class ReportPerformanceIssueAction extends Action { + + static readonly ID = 'workbench.action.reportPerformanceIssue'; + static readonly LABEL = nls.localize('reportPerformanceIssue', "Report Performance Issue"); + + constructor( + id: string, + label: string, + @IIntegrityService private integrityService: IIntegrityService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ITimerService private timerService: ITimerService + ) { + super(id, label); + } + + run(appendix?: string): Promise { + Promise.all([ + this.timerService.startupMetrics, + this.integrityService.isPure() + ]).then(([metrics, integrity]) => { + const issueUrl = this.generatePerformanceIssueUrl(metrics, product.reportIssueUrl, pkg.name, pkg.version, product.commit, product.date, integrity.isPure, appendix); + + window.open(issueUrl); + }); + + return Promise.resolve(true); + } + + private generatePerformanceIssueUrl(metrics: IStartupMetrics, baseUrl: string, name: string, version: string, _commit: string, _date: string, isPure: boolean, appendix?: string): string { + + if (!appendix) { + appendix = `Additional Steps to Reproduce(if any): + + 1. +2.`; + } + + let nodeModuleLoadTime: number; + if (this.environmentService.performance) { + nodeModuleLoadTime = LoaderStat.getLoaderStats().nodeRequireTotal; + } + + + const osVersion = `${os.type()} ${os.arch()} ${os.release()} `; + const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&'; + const body = encodeURIComponent( + `- VSCode Version: ${name} ${version} ${isPure ? '' : ' **[Unsupported]**'} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'}) + - OS Version: ${ osVersion} + - CPUs: ${ metrics.cpus.model} (${metrics.cpus.count} x ${metrics.cpus.speed}) + - Memory(System): ${ (metrics.totalmem / (1024 * 1024 * 1024)).toFixed(2)} GB(${(metrics.freemem / (1024 * 1024 * 1024)).toFixed(2)}GB free) < /code> + - Memory(Process): ${ (metrics.meminfo.workingSetSize / 1024).toFixed(2)} MB working set(${(metrics.meminfo.peakWorkingSetSize / 1024).toFixed(2)}MB peak, ${(metrics.meminfo.privateBytes / 1024).toFixed(2)}MB private, ${(metrics.meminfo.sharedBytes / 1024).toFixed(2)}MB shared) < /code> + - Load(avg): ${ metrics.loadavg.map(l => Math.round(l)).join(', ')} + - VM: ${ metrics.isVMLikelyhood}% + - Initial Startup: ${ metrics.initialStartup ? 'yes' : 'no'} + - Screen Reader: ${ metrics.hasAccessibilitySupport ? 'yes' : 'no'} + - Empty Workspace: ${ metrics.emptyWorkbench ? 'yes' : 'no'} + - Timings: + +${ this.generatePerformanceTable(metrics, nodeModuleLoadTime)} + +--- + + ${ appendix} ` + ); + + return `${baseUrl} ${queryStringPrefix} body = ${body} `; + } + + private generatePerformanceTable(metrics: IStartupMetrics, nodeModuleLoadTime?: number): string { + let tableHeader = `| Component | Task | Duration(ms) | Info | +| ---| ---| ---| ---| `; + + let table = ''; + forEach(Info.getTimerInfo(metrics, nodeModuleLoadTime), e => { + table += `| ${e.value.process}| ${e.key}| ${e.value.duration}| ${e.value.info}|\n`; + }); + + return `${tableHeader} \n${table} `; + } +} + +Registry + .as(Extensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(ShowStartupPerformance, ShowStartupPerformance.ID, ShowStartupPerformance.LABEL), 'Developer: Startup Performance', nls.localize('developer', "Developer")); diff --git a/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts b/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts index 771485b80f1..8c55379ff49 100644 --- a/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts +++ b/src/vs/workbench/parts/performance/electron-browser/performance.contribution.ts @@ -7,4 +7,5 @@ import './startupProfiler'; import './startupTimings'; +import './startupTimingsAppender'; import './stats'; diff --git a/src/vs/workbench/parts/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/parts/performance/electron-browser/startupProfiler.ts index 1b8c90733cd..52e522d7cb1 100644 --- a/src/vs/workbench/parts/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/parts/performance/electron-browser/startupProfiler.ts @@ -5,20 +5,19 @@ 'use strict'; +import { dirname, join } from 'path'; +import { basename } from 'vs/base/common/paths'; +import { del, exists, readdir, readFile } from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; -import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ReportPerformanceIssueAction } from 'vs/workbench/electron-browser/actions'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { join, dirname } from 'path'; -import { localize } from 'vs/nls'; -import { readdir, del, readFile } from 'vs/base/node/pfs'; -import { basename } from 'vs/base/common/paths'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { Extensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { ReportPerformanceIssueAction } from 'vs/workbench/parts/performance/electron-browser/actions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; class StartupProfiler implements IWorkbenchContribution { @@ -51,8 +50,20 @@ class StartupProfiler implements IWorkbenchContribution { const removeArgs: string[] = ['--prof-startup']; const markerFile = readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|'))) - .then(() => del(profileFilenamePrefix)) - .then(() => TPromise.timeout(1000)); + .then(() => del(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling + .then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped + const check = () => { + exists(profileFilenamePrefix).then(exists => { + if (exists) { + resolve(); + } else { + setTimeout(check, 500); + } + }); + }; + check(); + })) + .then(() => del(profileFilenamePrefix)); // (3) finally delete the file again markerFile.then(() => { return readdir(dir).then(files => files.filter(value => value.indexOf(prefix) === 0)); @@ -68,7 +79,7 @@ class StartupProfiler implements IWorkbenchContribution { }).then(res => { if (res.confirmed) { const action = this._instantiationService.createInstance(ReportPerformanceIssueAction, ReportPerformanceIssueAction.ID, ReportPerformanceIssueAction.LABEL); - TPromise.join([ + Promise.all([ this._windowsService.showItemInFolder(join(dir, files[0])), action.run(`:warning: Make sure to **attach** these files from your *home*-directory: :warning:\n${files.map(file => `-\`${file}\``).join('\n')}`) ]).then(() => { diff --git a/src/vs/workbench/parts/performance/electron-browser/startupTimings.ts b/src/vs/workbench/parts/performance/electron-browser/startupTimings.ts index 74eb0b8e04c..d4a974c0c38 100644 --- a/src/vs/workbench/parts/performance/electron-browser/startupTimings.ts +++ b/src/vs/workbench/parts/performance/electron-browser/startupTimings.ts @@ -5,22 +5,20 @@ 'use strict'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ILifecycleService, LifecyclePhase, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; -import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ITimerService } from 'vs/workbench/services/timer/common/timerService'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ILifecycleService, LifecyclePhase, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUpdateService } from 'vs/platform/update/common/update'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { Extensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import * as files from 'vs/workbench/parts/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { ILogService } from 'vs/platform/log/common/log'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { ITimerService, didUseCachedData } from 'vs/workbench/services/timer/electron-browser/timerService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; class StartupTimings implements IWorkbenchContribution { @@ -33,18 +31,14 @@ class StartupTimings implements IWorkbenchContribution { @IPanelService private readonly _panelService: IPanelService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IExtensionService private readonly _extensionService: IExtensionService, + @IUpdateService private readonly _updateService: IUpdateService, ) { this._reportVariedStartupTimes().then(undefined, onUnexpectedError); this._reportStandardStartupTimes().then(undefined, onUnexpectedError); } - private async _reportVariedStartupTimes(): TPromise { - await TPromise.join([ - this._extensionService.whenInstalledExtensionsRegistered(), - this._lifecycleService.when(LifecyclePhase.Eventually) - ]); + private async _reportVariedStartupTimes(): Promise { /* __GDPR__ "startupTimeVaried" : { "${include}": [ @@ -52,10 +46,10 @@ class StartupTimings implements IWorkbenchContribution { ] } */ - this._telemetryService.publicLog('startupTimeVaried', this._timerService.startupMetrics); + this._telemetryService.publicLog('startupTimeVaried', await this._timerService.startupMetrics); } - private async _reportStandardStartupTimes(): TPromise { + private async _reportStandardStartupTimes(): Promise { // check for standard startup: // * new window (no reload) // * just one window @@ -83,16 +77,14 @@ class StartupTimings implements IWorkbenchContribution { this._logService.info('no standard startup: panel is active'); return; } - if (!this._didUseCachedData()) { + if (!didUseCachedData()) { this._logService.info('no standard startup: not using cached data'); return; } - - // wait only know so that can check the restored state as soon as possible - await TPromise.join([ - this._extensionService.whenInstalledExtensionsRegistered(), - this._lifecycleService.when(LifecyclePhase.Eventually) - ]); + if (!await this._updateService.isLatestVersion()) { + this._logService.info('no standard startup: not running latest version'); + return; + } /* __GDPR__ "startupTime" : { @@ -101,27 +93,11 @@ class StartupTimings implements IWorkbenchContribution { ] } */ - this._telemetryService.publicLog('startupTime', this._timerService.startupMetrics); - this._logService.info('standard startup', this._timerService.startupMetrics); - } - - private _didUseCachedData(): boolean { - // We surely don't use cached data when we don't tell the loader to do so - if (!Boolean((global).require.getConfig().nodeCachedDataDir)) { - return false; - } - // whenever cached data is produced or rejected a onNodeCachedData-callback is invoked. That callback - // stores data in the `MonacoEnvironment.onNodeCachedData` global. See: - // https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/electron-browser/bootstrap/index.js#L219 - if (!isFalsyOrEmpty(MonacoEnvironment.onNodeCachedData)) { - return false; - } - return true; + const metrics = await this._timerService.startupMetrics; + this._telemetryService.publicLog('startupTime', metrics); + this._logService.info('standard startup', metrics); } } -declare type OnNodeCachedDataArgs = [{ errorCode: string, path: string, detail?: string }, { path: string, length: number }]; -declare const MonacoEnvironment: { onNodeCachedData: OnNodeCachedDataArgs[] }; - const registry = Registry.as(Extensions.Workbench); registry.registerWorkbenchContribution(StartupTimings, LifecyclePhase.Running); diff --git a/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts b/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts new file mode 100644 index 00000000000..676b7c3e4f3 --- /dev/null +++ b/src/vs/workbench/parts/performance/electron-browser/startupTimingsAppender.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITimerService, didUseCachedData } from 'vs/workbench/services/timer/electron-browser/timerService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { nfcall, timeout } from 'vs/base/common/async'; +import { appendFile, } from 'fs'; +import product from 'vs/platform/node/product'; + +class StartupTimingsAppender implements IWorkbenchContribution { + + constructor( + @ITimerService timerService: ITimerService, + @IWindowsService windowsService: IWindowsService, + @ILifecycleService lifecycleService: ILifecycleService, + @IEnvironmentService environmentService: IEnvironmentService, + ) { + + let appendTo = environmentService.args['prof-append-timers']; + if (!appendTo) { + // nothing to do + return; + } + + Promise.all([ + timerService.startupMetrics, + this._waitWhenNoCachedData(), + ]).then(([startupMetrics]) => { + return nfcall(appendFile, appendTo, `${startupMetrics.ellapsed}\t${product.nameLong}\t${product.commit || '0000000'}\n`); + }).then(() => { + windowsService.quit(); + }).catch(err => { + console.error(err); + windowsService.quit(); + }); + } + + private _waitWhenNoCachedData(): Promise { + // wait 15s for cached data to be produced + return !didUseCachedData() + ? timeout(15000) + : Promise.resolve(); + } +} + +const registry = Registry.as(Extensions.Workbench); +registry.registerWorkbenchContribution(StartupTimingsAppender, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts index ab0cfe703ed..9398a8c5046 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingWidgets.ts @@ -214,7 +214,9 @@ export class DefineKeybindingWidget extends Widget { this._domNode.setClassName('defineKeybindingWidget'); this._domNode.setWidth(DefineKeybindingWidget.WIDTH); this._domNode.setHeight(DefineKeybindingWidget.HEIGHT); - dom.append(this._domNode.domNode, dom.$('.message', null, nls.localize('defineKeybinding.initial', "Press desired key combination and then press ENTER."))); + + const message = nls.localize('defineKeybinding.initial', "Press desired key combination and then press ENTER."); + dom.append(this._domNode.domNode, dom.$('.message', null, message)); this._register(attachStylerCallback(this.themeService, { editorWidgetBackground, widgetShadow }, colors => { if (colors.editorWidgetBackground) { @@ -230,7 +232,7 @@ export class DefineKeybindingWidget extends Widget { } })); - this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingInputWidget, this._domNode.domNode, {})); + this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingInputWidget, this._domNode.domNode, { ariaLabel: message })); this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.onKeybinding(keybinding))); this._register(this._keybindingInputWidget.onEnter(() => this.hide())); this._register(this._keybindingInputWidget.onEscape(() => this.onCancel())); diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts index 6dc7fdc34fa..0f25d0a34a6 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditor.ts @@ -32,7 +32,7 @@ import { import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -74,6 +74,8 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor private searchFocusContextKey: IContextKey; private sortByPrecedence: Checkbox; + private ariaLabelElement: HTMLElement; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -100,6 +102,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor createEditor(parent: HTMLElement): void { const keybindingsEditorElement = DOM.append(parent, $('div', { class: 'keybindings-editor' })); + this.createAriaLabelElement(keybindingsEditorElement); this.createOverlayContainer(keybindingsEditorElement); this.createHeader(keybindingsEditorElement); this.createBody(keybindingsEditorElement); @@ -111,9 +114,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor setInput(input: KeybindingsEditorInput, options: EditorOptions, token: CancellationToken): Thenable { return super.setInput(input, options, token) - .then(() => { - this.render(options && options.preserveFocus, token); - }); + .then(() => this.render(options && options.preserveFocus, token)); } clearInput(): void { @@ -249,8 +250,12 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor return TPromise.as(null); } - search(filter: string): void { + focusSearch(): void { this.searchWidget.focus(); + } + + search(filter: string): void { + this.focusSearch(); this.searchWidget.setValue(filter); } @@ -266,6 +271,12 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor return TPromise.as(null); } + private createAriaLabelElement(parent: HTMLElement): void { + this.ariaLabelElement = DOM.append(parent, DOM.$('')); + this.ariaLabelElement.setAttribute('id', 'keybindings-editor-aria-label-element'); + this.ariaLabelElement.setAttribute('aria-live', 'assertive'); + } + private createOverlayContainer(parent: HTMLElement): void { this.overlayContainer = DOM.append(parent, $('.overlay-container')); this.overlayContainer.style.position = 'absolute'; @@ -291,16 +302,17 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { ariaLabel: localize('SearchKeybindings.AriaLabel', "Search keybindings"), placeholder: localize('SearchKeybindings.Placeholder', "Search keybindings"), - focusKey: this.searchFocusContextKey + focusKey: this.searchFocusContextKey, + ariaLabelledBy: 'keybindings-editor-aria-label-element' })); this._register(this.searchWidget.onDidChange(searchValue => this.delayedFiltering.trigger(() => this.filterKeybindings()))); this.sortByPrecedence = this._register(new Checkbox({ actionClassName: 'sort-by-precedence', isChecked: false, - onChange: () => this.renderKeybindingsEntries(false), title: localize('sortByPrecedene', "Sort by Precedence") })); + this._register(this.sortByPrecedence.onChange(() => this.renderKeybindingsEntries(false))); searchContainer.appendChild(this.sortByPrecedence.domNode); this.createOpenKeybindingsElement(this.headerContainer); @@ -394,6 +406,9 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor if (this.keybindingsEditorModel) { const filter = this.searchWidget.getValue(); const keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedence.checked); + + this.ariaLabelElement.setAttribute('aria-label', this.getAriaLabel(keybindingsEntries)); + if (keybindingsEntries.length === 0) { this.latestEmptyFilters.push(filter); } @@ -422,6 +437,14 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } } + private getAriaLabel(keybindingsEntries: IKeybindingItemEntry[]): string { + if (this.sortByPrecedence.checked) { + return localize('show sorted keybindings', "Showing {0} Keybindings in precedence order", keybindingsEntries.length); + } else { + return localize('show keybindings', "Showing {0} Keybindings in alphabetical order", keybindingsEntries.length); + } + } + private layoutKebindingsList(): void { const listHeight = this.dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/); this.keybindingsListContainer.style.height = `${listHeight}px`; @@ -591,7 +614,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } } -class Delegate implements IDelegate { +class Delegate implements IVirtualDelegate { getHeight(element: IListEntry) { if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) { @@ -644,6 +667,9 @@ class KeybindingHeaderRenderer implements IRenderer { renderElement(entry: IListEntry, index: number, template: any): void { } + disposeElement(): void { + } + disposeTemplate(template: any): void { } } @@ -681,6 +707,8 @@ class KeybindingItemRenderer implements IRenderer \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/action-remove.svg b/src/vs/workbench/parts/preferences/browser/media/action-remove.svg new file mode 100644 index 00000000000..fde34404d4e --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/action-remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/check-inverse.svg b/src/vs/workbench/parts/preferences/browser/media/check-inverse.svg new file mode 100644 index 00000000000..c225b2f597f --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/check-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/check.svg b/src/vs/workbench/parts/preferences/browser/media/check.svg new file mode 100644 index 00000000000..3f365c4800e --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/configure-inverse.svg b/src/vs/workbench/parts/preferences/browser/media/configure-inverse.svg new file mode 100644 index 00000000000..bbfbd366eb9 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/configure-inverse.svg @@ -0,0 +1 @@ +configure \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/configure.svg b/src/vs/workbench/parts/preferences/browser/media/configure.svg new file mode 100644 index 00000000000..c97bb48bdcc --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/configure.svg @@ -0,0 +1 @@ +configure \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/ellipsis-inverse.svg b/src/vs/workbench/parts/preferences/browser/media/ellipsis-inverse.svg new file mode 100644 index 00000000000..e3337557a23 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/ellipsis-inverse.svg @@ -0,0 +1 @@ +Ellipsis_bold_16x \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/ellipsis.svg b/src/vs/workbench/parts/preferences/browser/media/ellipsis.svg new file mode 100644 index 00000000000..e3f85623356 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/ellipsis.svg @@ -0,0 +1 @@ +Ellipsis_bold_16x \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css index da0b304cca7..5fc8abaf148 100644 --- a/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/parts/preferences/browser/media/keybindingsEditor.css @@ -45,16 +45,23 @@ .keybindings-editor > .keybindings-header .open-keybindings-container { margin-top: 10px; - opacity: 0.7; display: flex; } +.keybindings-editor > .keybindings-header .open-keybindings-container > div { + opacity: 0.7; +} + .keybindings-editor > .keybindings-header .open-keybindings-container > .file-name { text-decoration: underline; cursor: pointer; margin-left: 4px; } +.keybindings-editor > .keybindings-header .open-keybindings-container > .file-name:focus { + opacity: 1; +} + /** List based styling **/ .keybindings-editor > .keybindings-body .keybindings-list-container { @@ -152,6 +159,11 @@ font-weight: bold; } +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list:focus .monaco-list-row.selected > .column .highlight, +.keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list:focus .monaco-list-row.selected.focused > .column .highlight { + color: inherit; +} + .keybindings-editor > .keybindings-body > .keybindings-list-container .monaco-list-row > .column .monaco-action-bar { display: none; flex: 1; diff --git a/src/vs/workbench/parts/preferences/browser/media/preferences.css b/src/vs/workbench/parts/preferences/browser/media/preferences.css index ca03fe1df2b..caaa044e182 100644 --- a/src/vs/workbench/parts/preferences/browser/media/preferences.css +++ b/src/vs/workbench/parts/preferences/browser/media/preferences.css @@ -10,20 +10,6 @@ padding-top: 11px; } -.preferences-editor > .preferences-header > .new-settings-ad { - color: #6f6f6f; - margin-bottom: 4px; - font-size: 12px; -} - -.vs-dark .preferences-editor > .preferences-header > .new-settings-ad { - color: #bbbbbb; -} - -.hc-black .preferences-editor > .preferences-header > .new-settings-ad { - color: white; -} - .preferences-editor > .preferences-header > .new-settings-ad > .open-settings2-button { padding: 0; text-decoration: underline; diff --git a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css index 688fc6a36d9..ab6fff2fa21 100644 --- a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css @@ -3,77 +3,71 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.editor-instance#workbench\.editor\.settings2:focus { + outline: none; +} + .settings-editor { - padding-top: 11px; + padding: 11px 24px 0px; + max-width: 1000px; margin: auto; } /* header styling */ .settings-editor > .settings-header { - padding-left: 15px; - padding-right: 5px; - max-width: 800px; box-sizing: border-box; margin: auto; -} - -.settings-editor > .settings-header > .settings-preview-header { - margin-bottom: 5px; -} - -.settings-editor > .settings-header > .settings-preview-header .settings-preview-label { - opacity: .7; -} - -.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button, -.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button:hover, -.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button:active { - padding: 0; - text-decoration: underline; - display: inline; -} - -.settings-editor > .settings-header > .settings-advanced-customization { - opacity: .7; - margin-top: 10px; -} - -.settings-editor > .settings-header > .settings-preview-header > .settings-preview-warning { - text-align: right; - text-transform: uppercase; - background: rgba(136, 136, 136, 0.3); - border-radius: 2px; - font-size: 0.8em; - padding: 0 3px; - margin-right: 7px; + overflow: hidden; + padding-top: 3px; } .settings-editor > .settings-header > .search-container { position: relative; } -.settings-editor > .settings-header .search-container > .settings-search-input { - vertical-align: middle; +.vs .settings-editor > .settings-header > .search-container > .suggest-input-container { + border: 1px solid #ddd; } -.settings-editor > .settings-header .search-container > .settings-search-input > .monaco-inputbox { - height: 30px; - width: 100%; -} - -.settings-editor > .settings-header .search-container > .settings-search-input > .monaco-inputbox .input { - font-size: 14px; - padding-left: 10px; +.settings-editor > .settings-header > .search-container > .settings-count-widget { + margin: 6px 0px; + padding: 0px 8px; + border-radius: 2px; + position: absolute; + right: 10px; + top: 0; } .settings-editor > .settings-header > .settings-header-controls { - margin-top: 2px; - height: 30px; + height: 32px; display: flex; + border-bottom: solid 1px; + margin-top: 10px; } -.settings-editor > .settings-header .settings-tabs-widget > .monaco-action-bar .action-item:not(:first-child) .action-label { - margin-left: 14px; +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label { + opacity: 0.9; +} + +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label:hover { + opacity: 1; +} + +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label.checked { + font-weight: 500; + opacity: 1; +} + +.vs .settings-editor > .settings-header > .settings-header-controls { + border-color: #cccccc; +} + +.vs-dark .settings-editor > .settings-header > .settings-header-controls { + border-color: #3c3c3c; +} + +.settings-editor > .settings-header .settings-tabs-widget > .monaco-action-bar .action-item .action-label { + margin-right: 0px; } .settings-editor > .settings-header .settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon { @@ -83,79 +77,220 @@ .settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right { margin-left: auto; - padding-top: 3px; + padding-top: 4px; display: flex; } -.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right #configured-only-checkbox { - flex-shrink: 0; +.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right .toolbar-toggle-more { + display: block; + width: 22px; + height: 22px; + background-position: center; + background-repeat: no-repeat; + background-size: 16px; } -.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right .configured-only-label { - white-space: nowrap; - margin-right: 10px; - margin-left: 2px; - opacity: 0.7; +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item { + padding: 0px; /* padding must be on action-label because it has the bottom-border, because that's where the .checked class is */ } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper { - max-width: 800px; +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item .action-label { + text-transform: none; + font-size: 13px; + + padding-bottom: 7px; + padding-top: 7px; + padding-left: 8px; + padding-right: 8px; +} + +.settings-editor > .settings-body { + display: flex; margin: auto; + max-width: 1000px; } -.settings-editor > .settings-body .settings-tree-container .monaco-scrollable-element .shadow.top-left-corner { - left: calc((100% - 800px)/2); +.settings-editor > .settings-body > .no-results { + margin-top: 20px; + display: none; } -.settings-editor > .settings-body .settings-tree-container .monaco-scrollable-element .shadow { - left: calc((100% - 794px)/2); - width: 800px; +.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-tree-wrapper, +.settings-editor.narrow > .settings-body .settings-tree-container .monaco-tree-wrapper { + width: calc(100% - 11px); + margin-left: 0px; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree::before { - outline: none !important; +.settings-editor.no-toc-search > .settings-body > .settings-tree-container .setting-measure-container, +.settings-editor.narrow > .settings-body > .settings-tree-container .setting-measure-container { + width: calc(100% - 33px); + margin-left: 0px; +} + +.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper, +.settings-editor > .settings-body > .settings-tree-container .setting-measure-container { + /** 11px for scrollbar + 208px for TOC margin */ + width: calc(100% - 219px); + margin-left: 188px; + padding-left: 20px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-measure-container { + /* 20 from monaco-tree-wrapper + 20 from monaco-tree-row */ + padding-left: 40px; + width: calc(100% - 241px); + border: 1px solid transparent; +} + +.settings-editor > .settings-body .settings-tree-container .monaco-tree-rows { + width: calc(100% - 20px); +} + +.settings-editor > .settings-body .settings-tree-container .monaco-tree-row > .content::before { + /* Hide twisties */ + display: none !important; +} + +.settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { + position: absolute; + left: -23px; + top: 11px; + bottom: 0px; + width: 26px; +} + +.settings-editor > .settings-body .settings-tree-container .setting-toolbar-container > .monaco-toolbar { + display: none; +} + +.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .setting-toolbar-container:hover > .monaco-toolbar { + display: flex; +} + +.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .setting-item.focused .setting-toolbar-container > .monaco-toolbar { + display: flex; +} + +.settings-editor > .settings-body .settings-tree-container .setting-toolbar-container > .monaco-toolbar .toolbar-toggle-more { + display: block; + width: 22px; + height: 22px; + background-position: center; + background-repeat: no-repeat; + background-size: 16px; +} + +.vs .settings-editor > .settings-body .settings-tree-container .monaco-toolbar .toolbar-toggle-more { + background-image: url('configure.svg'); +} + +.vs-dark .settings-editor > .settings-body .settings-tree-container .monaco-toolbar .toolbar-toggle-more { + background-image: url('configure-inverse.svg'); +} + +.settings-editor > .settings-body .settings-toc-container { + position: absolute; + width: 160px; + margin-top: 16px; + padding-left: 5px; +} + +.settings-editor.no-toc-search > .settings-body .settings-toc-container, +.settings-editor.narrow > .settings-body .settings-toc-container { + display: none; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-scrollable-element > .shadow { + display: none; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row .content { + display: flex; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row .settings-toc-entry { + overflow: hidden; + text-overflow: ellipsis; + line-height: 22px; + opacity: 0.9; + flex-shrink: 1; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row .settings-toc-count { + display: none; + line-height: 22px; + opacity: 0.7; + margin-left: 3px; +} + +.settings-editor.search-mode > .settings-body .settings-toc-container .monaco-tree-row .settings-toc-count { + display: block; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row.has-children > .content::before { + opacity: 0.9; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row.has-children.selected > .content::before { + opacity: 1; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row .settings-toc-entry.no-results { + opacity: 0.5; +} + +.settings-editor > .settings-body .settings-toc-container .monaco-tree-row.selected .settings-toc-entry { + font-weight: bold; + opacity: 1; } .settings-editor > .settings-body .settings-tree-container { flex: 1; + margin-right: 1px; /* So the item doesn't blend into the edge of the view container */ + margin-top: 14px; border-spacing: 0; border-collapse: separate; position: relative; } -.settings-editor > .settings-body > .settings-tree-container .setting-item { +.settings-editor > .settings-body > .settings-tree-container .monaco-tree-row { + overflow: visible; /* so validation messages dont get clipped */ cursor: default; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item { + padding-top: 12px; + padding-bottom: 18px; + box-sizing: border-box; white-space: normal; - display: flex; height: 100%; - min-height: 75px; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item.odd:not(.focused):not(.selected):not(:hover), -.settings-editor > .settings-body > .settings-tree-container .monaco-tree:not(:focus) .setting-item.focused.odd:not(.selected):not(:hover), -.settings-editor > .settings-body > .settings-tree-container .monaco-tree:not(.focused) .setting-item.focused.odd:not(.selected):not(:hover) { - background-color: rgba(130, 130, 130, 0.04); -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item > .setting-item-left { - flex: 1; - padding-top: 3px; - padding-bottom: 12px; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item > .setting-item-right { - min-width: 180px; - margin: 21px 10px 0px 5px; } .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title { - line-height: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title .setting-item-is-configured-label { - font-style: italic; - opacity: 0.8; - margin-right: 7px; + +.settings-editor > .settings-body > .settings-tree-container .setting-item > .setting-item-modified-indicator { + display: none; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured > .setting-item-modified-indicator { + display: block; + content: ' '; + position: absolute; + width: 6px; + border-left-width: 2px; + border-left-style: solid; + left: 0px; + top: 15px; + bottom: 16px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item-bool.is-configured > .setting-item-modified-indicator { + bottom: 23px; } .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title .setting-item-overrides { @@ -167,106 +302,193 @@ margin-right: 7px; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-cat-label-container { + float: left; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-label, .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-category { - font-weight: bold; + font-weight: 600; + user-select: text; } .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-category { - opacity: 0.7; + opacity: 0.9; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-deprecation-message, .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { - opacity: 0.7; margin-top: 3px; - height: 36px; - overflow: hidden; - white-space: pre-wrap; + user-select: text; } -.settings-editor > .settings-body > .settings-tree-container .setting-measure-container.monaco-tree-row { - padding-left: 15px; - opacity: 0; - max-width: 800px; +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-deprecation-message { + position: absolute; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-expanded .setting-item-description, -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-measure-helper .setting-item-description { - height: initial; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button, -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button:hover, -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button:active { - margin: auto; - text-align: left; - text-decoration: underline; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button + .setting-reset-button.monaco-button { +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-validation-message { display: none; } +.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-validation-message { + display: block; + position: absolute; + padding: 5px; + box-sizing: border-box; + margin-top: -1px; + z-index: 1; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-validation-message { + width: 500px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-number .setting-item-validation-message { + width: 200px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-number input[type=number]::-webkit-inner-spin-button { + /* Hide arrow button that shows in type=number fields */ + -webkit-appearance: none !important; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown * { + margin: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown a:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + text-decoration: underline; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown a:hover { + text-decoration: underline; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown code { + line-height: 15px; /** For some reason, this is needed, otherwise will take up 20px height */ + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-measure-container.monaco-tree-row { + position: absolute; + visibility: hidden; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-measure-container .setting-item-description.measure-bool-description { + /* Allocate space for the checkbox control */ + margin-left: 27px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-enumDescription { + display: none; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-enumDescription, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-measure-helper .setting-item-enumDescription { + display: block; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-item-value-description { + display: flex; + cursor: pointer; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { + height: 18px; + width: 18px; + border: 1px solid transparent; + border-radius: 3px; + margin-right: 9px; + margin-left: 0px; + margin-top: 4px; + padding: 0px; + background-size: 16px !important; +} + +.vs .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox.checked { + background: url('check.svg') center center no-repeat; +} + +.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox.checked, +.hc-black .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox.checked { + background: url('check-inverse.svg') center center no-repeat; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value { + margin-top: 9px; + display: flex; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-number .setting-item-value > .setting-item-control { + min-width: 200px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-control { + width: 500px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-enum .setting-item-value > .setting-item-control, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-value > .setting-item-control { + min-width: initial; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-enum .setting-item-value > .setting-item-control > select { + width: 320px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value .edit-in-settings-button, +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value .edit-in-settings-button:hover, +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value .edit-in-settings-button:active { + text-align: left; + text-decoration: underline; + padding-left: 0px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item .monaco-select-box { - width: 100%; + width: initial; font: inherit; height: 26px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-reset-button.monaco-button { - text-align: left; - display: block; - visibility: hidden; - - padding-top: 0px; /* So focus outline doesn't overlap the control above */ -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-value > .setting-reset-button.monaco-button { - visibility: visible; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item .expand-indicator { - visibility: hidden; - position: absolute; - bottom: -2px; - width: calc(100% - 190px); - text-align: center; - opacity: .5; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-expandable .expand-indicator { - visibility: visible; -} - -.settings-editor > .settings-body > .settings-tree-container .all-settings { +.settings-editor > .settings-body > .settings-tree-container .setting-item-new-extensions { display: flex; } -.settings-editor > .settings-body > .settings-tree-container .all-settings .all-settings-button { +.settings-editor > .settings-body > .settings-tree-container .setting-item-new-extensions .settings-new-extensions-button { margin: auto; + width: initial; + padding: 4px 10px; } -.settings-editor > .settings-body > .settings-tree-container .all-settings .all-settings-button .monaco-button { - padding-left: 10px; - padding-right: 10px; +.settings-editor > .settings-body > .settings-tree-container .group-title, +.settings-editor > .settings-body > .settings-tree-container .setting-item { + padding-left: 9px; + padding-right: 9px; } -/* - Ensure the is-configured indicators can appear outside of the list items themselves: - - Disable overflow: hidden on the listrow - - Allocate some space with a margin on the list-row - - Make up for that space with a negative margin on the settings-body - - This is risky, consider a different approach -*/ -.settings-editor .settings-tree-container .setting-item { - overflow: visible; +.settings-editor > .settings-body > .settings-tree-container .group-title { + cursor: default; } .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { margin: 0px; - padding: 5px 0px; - font-size: 13px; + font-weight: 600; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { + padding-top: 23px; + font-size: 24px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-level-2 { + padding-top: 32px; + font-size: 20px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { + padding-top: 7px; } .settings-editor > .settings-body .settings-feedback-button { diff --git a/src/vs/workbench/parts/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/parts/preferences/browser/media/settingsWidgets.css new file mode 100644 index 00000000000..deb7ff3d83c --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/settingsWidgets.css @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-item-value > .setting-item-control { + width: 100%; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-pattern { + margin-right: 3px; + margin-left: 2px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-pattern, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-sibling { + display: inline-block; + line-height: 22px; + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-sibling { + opacity: 0.7; + margin-left: 0.5em; + font-size: 0.9em; + white-space: pre; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar { + display: none; + position: absolute; + right: 0px; + margin-top: 1px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row { + position: relative; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:focus { + outline: none; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover .monaco-action-bar, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected .monaco-action-bar { + display: block; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .action-label { + width: 16px; + height: 16px; + padding: 2px; + margin-right: 2px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { + margin-right: 4px; +} + +.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { + background: url(edit.svg) center center no-repeat; +} + +.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { + background: url(edit_inverse.svg) center center no-repeat; +} + +.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-remove { + background: url(action-remove.svg) center center no-repeat; +} + +.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-remove { + background: url(action-remove-dark.svg) center center no-repeat; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .monaco-text-button { + width: initial; + padding: 2px 14px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-item-control.setting-exclude-new-mode .setting-exclude-new-row { + display: none; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .monaco-text-button.setting-exclude-addButton { + margin-right: 10px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-patternInput, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-siblingInput { + height: 22px; + max-width: 300px; + display: inline-block; + margin-right: 10px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-okButton { + margin-right: 10px; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-widget { + margin-bottom: 1px; +} diff --git a/src/vs/workbench/parts/preferences/browser/media/sort_precedence.svg b/src/vs/workbench/parts/preferences/browser/media/sort_precedence.svg index 25b013f5c32..07e6d6b84ba 100644 --- a/src/vs/workbench/parts/preferences/browser/media/sort_precedence.svg +++ b/src/vs/workbench/parts/preferences/browser/media/sort_precedence.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/sort_precedence_inverse.svg b/src/vs/workbench/parts/preferences/browser/media/sort_precedence_inverse.svg index 1b4301884a6..92c9a64dbc4 100644 --- a/src/vs/workbench/parts/preferences/browser/media/sort_precedence_inverse.svg +++ b/src/vs/workbench/parts/preferences/browser/media/sort_precedence_inverse.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts index 4c19890f0f3..87b1efc0616 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; +import { getIconClasses } from 'vs/workbench/browser/labels'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; export class OpenRawDefaultSettingsAction extends Action { @@ -37,22 +39,21 @@ export class OpenRawDefaultSettingsAction extends Action { export class OpenSettings2Action extends Action { public static readonly ID = 'workbench.action.openSettings2'; - public static readonly LABEL = nls.localize('openSettings2', "Open Settings (Preview)"); + public static readonly LABEL = nls.localize('openSettings2', "Open Settings (UI)"); constructor( id: string, label: string, - @IPreferencesService private preferencesService2: IPreferencesService + @IPreferencesService private preferencesService: IPreferencesService ) { super(id, label); } public run(event?: any): TPromise { - return this.preferencesService2.openSettings2(); + return this.preferencesService.openSettings(false); } } - export class OpenSettingsAction extends Action { public static readonly ID = 'workbench.action.openSettings'; @@ -71,6 +72,24 @@ export class OpenSettingsAction extends Action { } } +export class OpenSettingsJsonAction extends Action { + + public static readonly ID = 'workbench.action.openSettingsJson'; + public static readonly LABEL = nls.localize('openSettingsJson', "Open Settings (JSON)"); + + constructor( + id: string, + label: string, + @IPreferencesService private preferencesService: IPreferencesService + ) { + super(id, label); + } + + public run(event?: any): TPromise { + return this.preferencesService.openSettings(true); + } +} + export class OpenGlobalSettingsAction extends Action { public static readonly ID = 'workbench.action.openGlobalSettings'; @@ -79,7 +98,7 @@ export class OpenGlobalSettingsAction extends Action { constructor( id: string, label: string, - @IPreferencesService private preferencesService: IPreferencesService + @IPreferencesService private preferencesService: IPreferencesService, ) { super(id, label); } @@ -125,6 +144,24 @@ export class OpenGlobalKeybindingsFileAction extends Action { } } +export class OpenDefaultKeybindingsFileAction extends Action { + + public static readonly ID = 'workbench.action.openDefaultKeybindingsFile'; + public static readonly LABEL = nls.localize('openDefaultKeybindingsFile', "Open Default Keyboard Shortcuts File"); + + constructor( + id: string, + label: string, + @IPreferencesService private preferencesService: IPreferencesService + ) { + super(id, label); + } + + public run(event?: any): TPromise { + return this.preferencesService.openDefaultKeybindingsFile(); + } +} + export class OpenWorkspaceSettingsAction extends Action { public static readonly ID = 'workbench.action.openWorkspaceSettings'; @@ -136,7 +173,7 @@ export class OpenWorkspaceSettingsAction extends Action { id: string, label: string, @IPreferencesService private preferencesService: IPreferencesService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, ) { super(id, label); this.update(); @@ -171,7 +208,8 @@ export class OpenFolderSettingsAction extends Action { id: string, label: string, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @ICommandService private commandService: ICommandService + @IPreferencesService private preferencesService: IPreferencesService, + @ICommandService private commandService: ICommandService, ) { super(id, label); this.update(); @@ -187,8 +225,9 @@ export class OpenFolderSettingsAction extends Action { return this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID) .then(workspaceFolder => { if (workspaceFolder) { - return this.commandService.executeCommand(OPEN_FOLDER_SETTINGS_COMMAND, workspaceFolder.uri); + return this.preferencesService.openFolderSettings(workspaceFolder.uri); } + return null; }); } @@ -207,8 +246,9 @@ export class ConfigureLanguageBasedSettingsAction extends Action { constructor( id: string, label: string, + @IModelService private modelService: IModelService, @IModeService private modeService: IModeService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IPreferencesService private preferencesService: IPreferencesService ) { super(id, label); @@ -216,7 +256,7 @@ export class ConfigureLanguageBasedSettingsAction extends Action { public run(): TPromise { const languages = this.modeService.getRegisteredLanguageNames(); - const picks: IPickOpenEntry[] = languages.sort().map((lang, index) => { + const picks: IQuickPickItem[] = languages.sort().map((lang, index) => { let description: string = nls.localize('languageDescriptionConfigured', "({0})", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); // construct a fake resource to be able to show nice icons if any let fakeResource: URI; @@ -229,14 +269,14 @@ export class ConfigureLanguageBasedSettingsAction extends Action { fakeResource = URI.file(filenames[0]); } } - return { + return { label: lang, - resource: fakeResource, + iconClasses: getIconClasses(this.modelService, this.modeService, fakeResource), description - }; + } as IQuickPickItem; }); - return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language") }) + return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language") }) .then(pick => { if (pick) { return this.modeService.getOrCreateModeByLanguageName(pick.label) diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 49289f5eae3..bad8ffc25ad 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -4,22 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { VSash } from 'vs/base/browser/ui/sash/sash'; +import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Widget } from 'vs/base/browser/ui/widget'; import * as arrays from 'vs/base/common/arrays'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { getErrorMessage, isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { ArrayNavigator } from 'vs/base/common/iterator'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Command, EditorExtensionsRegistry, IEditorContributionCtor, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorExtensionsRegistry, IEditorContributionCtor, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -30,9 +29,8 @@ import { MessageController } from 'vs/editor/contrib/message/messageController'; import { SelectionHighlighter } from 'vs/editor/contrib/multicursor/multicursor'; import * as nls from 'vs/nls'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -50,21 +48,21 @@ import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorMo import { PREFERENCES_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { DefaultSettingsRenderer, FolderSettingsRenderer, IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from 'vs/workbench/parts/preferences/browser/preferencesRenderers'; import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, IPreferencesSearchService, ISearchProvider, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, SETTINGS_EDITOR_COMMAND_SEARCH } from 'vs/workbench/parts/preferences/common/preferences'; -import { IFilterResult, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IFilterResult, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsGroup, SettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel, SettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { IWindowService } from 'vs/platform/windows/common/windows'; export class PreferencesEditor extends BaseEditor { public static readonly ID: string = PREFERENCES_EDITOR_ID; private defaultSettingsEditorContextKey: IContextKey; - private focusSettingsContextKey: IContextKey; + private searchFocusContextKey: IContextKey; private headerContainer: HTMLElement; private searchWidget: SearchWidget; private sideBySidePreferencesWidget: SideBySidePreferencesWidget; @@ -77,6 +75,18 @@ export class PreferencesEditor extends BaseEditor { private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null; + get minimumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.minimumWidth : 0; } + get maximumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.maximumWidth : Number.POSITIVE_INFINITY; } + + // these setters need to exist because this extends from BaseEditor + set minimumWidth(value: number) { /*noop*/ } + set maximumWidth(value: number) { /*noop*/ } + + readonly minimumHeight = 260; + + private _onDidCreateWidget = new Emitter<{ width: number; height: number; }>(); + readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; }> = this._onDidCreateWidget.event; + constructor( @IPreferencesService private preferencesService: IPreferencesService, @ITelemetryService telemetryService: ITelemetryService, @@ -88,7 +98,7 @@ export class PreferencesEditor extends BaseEditor { ) { super(PreferencesEditor.ID, telemetryService, themeService); this.defaultSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(this.contextKeyService); - this.focusSettingsContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); + this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(100); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -98,25 +108,12 @@ export class PreferencesEditor extends BaseEditor { DOM.addClass(parent, 'preferences-editor'); this.headerContainer = DOM.append(parent, DOM.$('.preferences-header')); - const advertisement = DOM.append(this.headerContainer, DOM.$('.new-settings-ad')); - const advertisementLabel = DOM.append(advertisement, DOM.$('span.new-settings-ad-label')); - advertisementLabel.textContent = nls.localize('advertisementLabel', "Try a preview of our ") + ' '; - const openSettings2Button = this._register(new Button(advertisement, { title: true, buttonBackground: null, buttonHoverBackground: null })); - openSettings2Button.style({ - buttonBackground: null, - buttonForeground: null, - buttonBorder: null, - buttonHoverBackground: null - }); - openSettings2Button.label = nls.localize('openSettings2Label', "new settings editor"); - openSettings2Button.element.classList.add('open-settings2-button'); - this._register(openSettings2Button.onDidClick(() => this.preferencesService.openSettings2())); - this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.headerContainer, { ariaLabel: nls.localize('SearchSettingsWidget.AriaLabel', "Search settings"), placeholder: nls.localize('SearchSettingsWidget.Placeholder', "Search Settings"), - focusKey: this.focusSettingsContextKey, - showResultCount: true + focusKey: this.searchFocusContextKey, + showResultCount: true, + ariaLive: 'assertive' })); this._register(this.searchWidget.onDidChange(value => this.onInputChanged())); this._register(this.searchWidget.onFocus(() => this.lastFocusedWidget = this.searchWidget)); @@ -124,6 +121,7 @@ export class PreferencesEditor extends BaseEditor { const editorsContainer = DOM.append(parent, DOM.$('.preferences-editors-container')); this.sideBySidePreferencesWidget = this._register(this.instantiationService.createInstance(SideBySidePreferencesWidget, editorsContainer)); + this._onDidCreateWidget.fire(); this._register(this.sideBySidePreferencesWidget.onFocus(() => this.lastFocusedWidget = this.sideBySidePreferencesWidget)); this._register(this.sideBySidePreferencesWidget.onDidSettingsTargetChange(target => this.switchSettings(target))); @@ -154,8 +152,12 @@ export class PreferencesEditor extends BaseEditor { this.preferencesRenderers.editFocusedPreference(); } - public setInput(newInput: PreferencesEditorInput, options: EditorOptions, token: CancellationToken): Thenable { + public setInput(newInput: PreferencesEditorInput, options: SettingsEditorOptions, token: CancellationToken): Thenable { this.defaultSettingsEditorContextKey.set(true); + if (options && options.query) { + this.focusSearch(options.query); + } + return super.setInput(newInput, options, token).then(() => this.updateInput(newInput, options, token)); } @@ -196,10 +198,6 @@ export class PreferencesEditor extends BaseEditor { super.clearInput(); } - public supportsCenteredLayout(): boolean { - return false; - } - protected setEditorVisible(visible: boolean, group: IEditorGroup): void { this.sideBySidePreferencesWidget.setEditorVisible(visible, group); super.setEditorVisible(visible, group); @@ -234,8 +232,8 @@ export class PreferencesEditor extends BaseEditor { private triggerSearch(query: string): TPromise { if (query) { return TPromise.join([ - this.localSearchDelayer.trigger(() => this.preferencesRenderers.localFilterPreferences(query)), - this.remoteSearchThrottle.trigger(() => this.progressService.showWhile(this.preferencesRenderers.remoteSearchPreferences(query), 500)) + this.localSearchDelayer.trigger(() => this.preferencesRenderers.localFilterPreferences(query).then(() => { })), + this.remoteSearchThrottle.trigger(() => TPromise.wrap(this.progressService.showWhile(this.preferencesRenderers.remoteSearchPreferences(query), 500))) ]) as TPromise; } else { // When clearing the input, update immediately to clear it @@ -253,13 +251,13 @@ export class PreferencesEditor extends BaseEditor { this.focus(); } const promise = this.input && this.input.isDirty() ? this.input.save() : TPromise.as(true); - promise.done(value => { + promise.then(value => { if (target === ConfigurationTarget.USER) { - this.preferencesService.switchSettings(ConfigurationTarget.USER, this.preferencesService.userSettingsResource); + this.preferencesService.switchSettings(ConfigurationTarget.USER, this.preferencesService.userSettingsResource, true); } else if (target === ConfigurationTarget.WORKSPACE) { - this.preferencesService.switchSettings(ConfigurationTarget.WORKSPACE, this.preferencesService.workspaceSettingsResource); + this.preferencesService.switchSettings(ConfigurationTarget.WORKSPACE, this.preferencesService.workspaceSettingsResource, true); } else if (target instanceof URI) { - this.preferencesService.switchSettings(ConfigurationTarget.WORKSPACE_FOLDER, target); + this.preferencesService.switchSettings(ConfigurationTarget.WORKSPACE_FOLDER, target, true); } }); } @@ -328,6 +326,11 @@ export class PreferencesEditor extends BaseEditor { this._lastReportedFilter = filter; } } + + dispose(): void { + this._onDidCreateWidget.dispose(); + super.dispose(); + } } class SettingsNavigator extends ArrayNavigator { @@ -359,7 +362,7 @@ class PreferencesRenderersController extends Disposable { private _editablePreferencesRendererDisposables: IDisposable[] = []; private _settingsNavigator: SettingsNavigator; - private _remoteFilterInProgress: TPromise; + private _remoteFilterCancelToken: CancellationTokenSource; private _prefsModelsForSearch = new Map(); private _currentLocalSearchProvider: ISearchProvider; @@ -422,9 +425,11 @@ class PreferencesRenderersController extends Disposable { } } - private async _onEditableContentDidChange(): TPromise { - await this.localFilterPreferences(this._lastQuery, true); - await this.remoteSearchPreferences(this._lastQuery, true); + private async _onEditableContentDidChange(): Promise { + const foundExactMatch = await this.localFilterPreferences(this._lastQuery, true); + if (!foundExactMatch) { + await this.remoteSearchPreferences(this._lastQuery, true); + } } onHidden(): void { @@ -433,17 +438,25 @@ class PreferencesRenderersController extends Disposable { } remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise { - if (this._remoteFilterInProgress && this._remoteFilterInProgress.cancel) { - // Resolved/rejected promises have no .cancel() - this._remoteFilterInProgress.cancel(); + if (this.lastFilterResult && this.lastFilterResult.exactMatch) { + // Skip and clear remote search + query = ''; + } + + if (this._remoteFilterCancelToken) { + this._remoteFilterCancelToken.cancel(); + this._remoteFilterCancelToken.dispose(); + this._remoteFilterCancelToken = null; } this._currentRemoteSearchProvider = (updateCurrentResults && this._currentRemoteSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query); - this._remoteFilterInProgress = this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1, updateCurrentResults); - - return this._remoteFilterInProgress.then(() => { - this._remoteFilterInProgress = null; + this._remoteFilterCancelToken = new CancellationTokenSource(); + return this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1, this._remoteFilterCancelToken.token, updateCurrentResults).then(() => { + if (this._remoteFilterCancelToken) { + this._remoteFilterCancelToken.dispose(); + this._remoteFilterCancelToken = null; + } }, err => { if (isPromiseCanceledError(err)) { return null; @@ -453,26 +466,26 @@ class PreferencesRenderersController extends Disposable { }); } - localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { + localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { if (this._settingsNavigator) { this._settingsNavigator.reset(); } this._currentLocalSearchProvider = (updateCurrentResults && this._currentLocalSearchProvider) || this.preferencesSearchService.getLocalSearchProvider(query); - return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0, updateCurrentResults); + return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0, undefined, updateCurrentResults); } - private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, editableContentOnly?: boolean): TPromise { + private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken, editableContentOnly?: boolean): TPromise { this._lastQuery = query; - const filterPs: TPromise[] = [this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)]; + const filterPs: TPromise[] = [this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder, token)]; if (!editableContentOnly) { filterPs.push( - this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)); + this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder, token)); + filterPs.push( + this.searchAllSettingsTargets(query, searchProvider, groupId, groupLabel, groupOrder, token).then(() => null)); } - filterPs.push(this.searchAllSettingsTargets(query, searchProvider, groupId, groupLabel, groupOrder)); - return TPromise.join(filterPs).then(results => { let [editableFilterResult, defaultFilterResult] = results; @@ -481,36 +494,36 @@ class PreferencesRenderersController extends Disposable { } this.consolidateAndUpdate(defaultFilterResult, editableFilterResult); - if (defaultFilterResult) { - this._lastFilterResult = defaultFilterResult; - } + this._lastFilterResult = defaultFilterResult; + + return defaultFilterResult && defaultFilterResult.exactMatch; }); } - private searchAllSettingsTargets(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { + private searchAllSettingsTargets(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken): TPromise { const searchPs = [ - this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.WORKSPACE, groupId, groupLabel, groupOrder), - this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.USER, groupId, groupLabel, groupOrder) + this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.WORKSPACE, groupId, groupLabel, groupOrder, token), + this.searchSettingsTarget(query, searchProvider, ConfigurationTarget.USER, groupId, groupLabel, groupOrder, token) ]; for (const folder of this.workspaceContextService.getWorkspace().folders) { const folderSettingsResource = this.preferencesService.getFolderSettingsResource(folder.uri); - searchPs.push(this.searchSettingsTarget(query, searchProvider, folderSettingsResource, groupId, groupLabel, groupOrder)); + searchPs.push(this.searchSettingsTarget(query, searchProvider, folderSettingsResource, groupId, groupLabel, groupOrder, token)); } return TPromise.join(searchPs).then(() => { }); } - private searchSettingsTarget(query: string, provider: ISearchProvider, target: SettingsTarget, groupId: string, groupLabel: string, groupOrder: number): TPromise { + private searchSettingsTarget(query: string, provider: ISearchProvider, target: SettingsTarget, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken): Promise { if (!query) { // Don't open the other settings targets when query is empty this._onDidFilterResultsCountChange.fire({ target, count: 0 }); - return TPromise.wrap(null); + return Promise.resolve(null); } return this.getPreferencesEditorModel(target).then(model => { - return model && this._filterOrSearchPreferencesModel('', model, provider, groupId, groupLabel, groupOrder); + return model && this._filterOrSearchPreferencesModel('', model, provider, groupId, groupLabel, groupOrder, token); }).then(result => { const count = result ? this._flatten(result.filteredGroups).length : 0; this._onDidFilterResultsCountChange.fire({ target, count }); @@ -523,7 +536,7 @@ class PreferencesRenderersController extends Disposable { }); } - private async getPreferencesEditorModel(target: SettingsTarget): TPromise { + private async getPreferencesEditorModel(target: SettingsTarget): Promise { const resource = target === ConfigurationTarget.USER ? this.preferencesService.userSettingsResource : target === ConfigurationTarget.WORKSPACE ? this.preferencesService.workspaceSettingsResource : target; @@ -568,20 +581,20 @@ class PreferencesRenderersController extends Disposable { } } - private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { + private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken): TPromise { if (!preferencesRenderer) { return TPromise.wrap(null); } const model = preferencesRenderer.preferencesModel; - return this._filterOrSearchPreferencesModel(filter, model, provider, groupId, groupLabel, groupOrder).then(filterResult => { + return this._filterOrSearchPreferencesModel(filter, model, provider, groupId, groupLabel, groupOrder, token).then(filterResult => { preferencesRenderer.filterPreferences(filterResult); return filterResult; }); } - private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise { - const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null); + private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number, token?: CancellationToken): TPromise { + const searchP = provider ? provider.searchModel(model, token) : TPromise.wrap(null); return searchP .then(null, err => { if (isPromiseCanceledError(err)) { @@ -603,6 +616,10 @@ class PreferencesRenderersController extends Disposable { } }) .then(searchResult => { + if (token && token.isCancellationRequested) { + searchResult = null; + } + const filterResult = searchResult ? model.updateResultGroup(groupId, { id: groupId, @@ -614,6 +631,7 @@ class PreferencesRenderersController extends Disposable { if (filterResult) { filterResult.query = filter; + filterResult.exactMatch = searchResult && searchResult.exactMatch; } return filterResult; @@ -741,7 +759,7 @@ class PreferencesRenderersController extends Disposable { class SideBySidePreferencesWidget extends Widget { - private dimension: DOM.Dimension; + private dimension: DOM.Dimension = new DOM.Dimension(0, 0); private defaultPreferencesHeader: HTMLElement; private defaultPreferencesEditor: DefaultPreferencesEditor; @@ -758,30 +776,29 @@ class SideBySidePreferencesWidget extends Widget { readonly onDidSettingsTargetChange: Event = this._onDidSettingsTargetChange.event; private lastFocusedEditor: BaseEditor; + private splitview: SplitView; - private sash: VSash; + get minimumWidth(): number { return this.splitview.minimumSize; } + get maximumWidth(): number { return this.splitview.maximumSize; } constructor( - parent: HTMLElement, + parentElement: HTMLElement, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @IPreferencesService private preferencesService: IPreferencesService, ) { super(); - this.create(parent); - } - private create(parentElement: HTMLElement): void { DOM.addClass(parentElement, 'side-by-side-preferences-editor'); - this.createSash(parentElement); - this.defaultPreferencesEditorContainer = DOM.append(parentElement, DOM.$('.default-preferences-editor-container')); - this.defaultPreferencesEditorContainer.style.position = 'absolute'; + this.splitview = new SplitView(parentElement, { orientation: Orientation.HORIZONTAL }); + this._register(this.splitview); + this._register(this.splitview.onDidSashReset(() => this.splitview.distributeViewSizes())); + + this.defaultPreferencesEditorContainer = DOM.$('.default-preferences-editor-container'); const defaultPreferencesHeaderContainer = DOM.append(this.defaultPreferencesEditorContainer, DOM.$('.preferences-header-container')); - defaultPreferencesHeaderContainer.style.height = '30px'; - defaultPreferencesHeaderContainer.style.marginBottom = '4px'; this.defaultPreferencesHeader = DOM.append(defaultPreferencesHeaderContainer, DOM.$('div.default-preferences-header')); this.defaultPreferencesHeader.textContent = nls.localize('defaultSettings', "Default Settings"); @@ -789,11 +806,16 @@ class SideBySidePreferencesWidget extends Widget { this.defaultPreferencesEditor.create(this.defaultPreferencesEditorContainer); (this.defaultPreferencesEditor.getControl()).onDidFocusEditorWidget(() => this.lastFocusedEditor = this.defaultPreferencesEditor); - this.editablePreferencesEditorContainer = DOM.append(parentElement, DOM.$('.editable-preferences-editor-container')); - this.editablePreferencesEditorContainer.style.position = 'absolute'; + this.splitview.addView({ + element: this.defaultPreferencesEditorContainer, + layout: size => this.defaultPreferencesEditor.layout(new DOM.Dimension(size, this.dimension.height - 34 /* height of header container */)), + minimumSize: 220, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.None + }, Sizing.Distribute); + + this.editablePreferencesEditorContainer = DOM.$('.editable-preferences-editor-container'); const editablePreferencesHeaderContainer = DOM.append(this.editablePreferencesEditorContainer, DOM.$('.preferences-header-container')); - editablePreferencesHeaderContainer.style.height = '30px'; - editablePreferencesHeaderContainer.style.marginBottom = '4px'; this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, editablePreferencesHeaderContainer)); this._register(this.settingsTargetsWidget.onDidTargetChange(target => this._onDidSettingsTargetChange.fire(target))); @@ -807,21 +829,28 @@ class SideBySidePreferencesWidget extends Widget { } })); + this.splitview.addView({ + element: this.editablePreferencesEditorContainer, + layout: size => this.editablePreferencesEditor && this.editablePreferencesEditor.layout(new DOM.Dimension(size, this.dimension.height - 34 /* height of header container */)), + minimumSize: 220, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.None + }, Sizing.Distribute); + const focusTracker = this._register(DOM.trackFocus(parentElement)); this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); } - public setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions, token: CancellationToken): TPromise<{ defaultPreferencesRenderer: IPreferencesRenderer, editablePreferencesRenderer: IPreferencesRenderer }> { + public setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions, token: CancellationToken): TPromise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer }> { this.getOrCreateEditablePreferencesEditor(editablePreferencesEditorInput); this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.getResource()); - this.dolayout(this.sash.getVerticalSashLeft()); return TPromise.join([ this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.getResource(), options, token), this.updateInput(this.editablePreferencesEditor, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.getResource(), options, token) ]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { if (token.isCancellationRequested) { - return void 0; + return {}; } this.defaultPreferencesHeader.textContent = defaultPreferencesRenderer && this.getDefaultPreferencesHeaderText((defaultPreferencesRenderer.preferencesModel).target); @@ -845,9 +874,9 @@ class SideBySidePreferencesWidget extends Widget { this.settingsTargetsWidget.setResultCount(settingsTarget, count); } - public layout(dimension: DOM.Dimension): void { + public layout(dimension: DOM.Dimension = this.dimension): void { this.dimension = dimension; - this.sash.setDimenesion(this.dimension); + this.splitview.layout(dimension.width); } public focus(): void { @@ -888,6 +917,7 @@ class SideBySidePreferencesWidget extends Widget { this.editablePreferencesEditor.create(this.editablePreferencesEditorContainer); (this.editablePreferencesEditor.getControl()).onDidFocusEditorWidget(() => this.lastFocusedEditor = this.editablePreferencesEditor); this.lastFocusedEditor = this.editablePreferencesEditor; + this.layout(); return editor; } @@ -903,30 +933,6 @@ class SideBySidePreferencesWidget extends Widget { }); } - private createSash(parentElement: HTMLElement): void { - this.sash = this._register(new VSash(parentElement, 220)); - this._register(this.sash.onPositionChange(position => this.dolayout(position))); - } - - private dolayout(splitPoint: number): void { - if (!this.editablePreferencesEditor || !this.dimension) { - return; - } - const masterEditorWidth = this.dimension.width - splitPoint; - const detailsEditorWidth = this.dimension.width - masterEditorWidth; - - this.defaultPreferencesEditorContainer.style.width = `${detailsEditorWidth}px`; - this.defaultPreferencesEditorContainer.style.height = `${this.dimension.height}px`; - this.defaultPreferencesEditorContainer.style.left = '0px'; - - this.editablePreferencesEditorContainer.style.width = `${masterEditorWidth}px`; - this.editablePreferencesEditorContainer.style.height = `${this.dimension.height}px`; - this.editablePreferencesEditorContainer.style.left = `${splitPoint}px`; - - this.defaultPreferencesEditor.layout(new DOM.Dimension(detailsEditorWidth, this.dimension.height - 34 /* height of header container */)); - this.editablePreferencesEditor.layout(new DOM.Dimension(masterEditorWidth, this.dimension.height - 34 /* height of header container */)); - } - private getSettingsTarget(resource: URI): SettingsTarget { if (this.preferencesService.userSettingsResource.toString() === resource.toString()) { return ConfigurationTarget.USER; @@ -974,9 +980,10 @@ export class DefaultPreferencesEditor extends BaseTextEditor { @IThemeService themeService: IThemeService, @ITextFileService textFileService: ITextFileService, @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IWindowService windowService: IWindowService ) { - super(DefaultPreferencesEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService); + super(DefaultPreferencesEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService); } private static _getContributions(): IEditorContributionCtor[] { @@ -990,8 +997,8 @@ export class DefaultPreferencesEditor extends BaseTextEditor { const editor = this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, { contributions: DefaultPreferencesEditor._getContributions() }); // Inform user about editor being readonly if user starts type - this.toUnbind.push(editor.onDidType(() => this.showReadonlyHint(editor))); - this.toUnbind.push(editor.onDidPaste(() => this.showReadonlyHint(editor))); + this._register(editor.onDidType(() => this.showReadonlyHint(editor))); + this._register(editor.onDidPaste(() => this.showReadonlyHint(editor))); return editor; } @@ -1054,10 +1061,6 @@ export class DefaultPreferencesEditor extends BaseTextEditor { this.getControl().layout(dimension); } - public supportsCenteredLayout(): boolean { - return false; - } - protected getAriaLabel(): string { return nls.localize('preferencesAriaLabel', "Default preferences. Readonly text editor."); } @@ -1241,117 +1244,3 @@ class SettingsEditorContribution extends AbstractSettingsEditorContribution impl } registerEditorContribution(SettingsEditorContribution); - -abstract class SettingsCommand extends Command { - - protected getPreferencesEditor(accessor: ServicesAccessor): PreferencesEditor { - const activeControl = accessor.get(IEditorService).activeControl; - if (activeControl instanceof PreferencesEditor) { - return activeControl; - } - return null; - - } - -} -class StartSearchDefaultSettingsCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSearch(); - } - } - -} -const command = new StartSearchDefaultSettingsCommand({ - id: SETTINGS_EDITOR_COMMAND_SEARCH, - precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR), - kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_F } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(command.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class FocusSettingsFileEditorCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSettingsFileEditor(); - } - } - -} -const focusSettingsFileEditorCommand = new FocusSettingsFileEditorCommand({ - id: SETTINGS_EDITOR_COMMAND_FOCUS_FILE, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, - kbOpts: { primary: KeyCode.DownArrow } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(focusSettingsFileEditorCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class ClearSearchResultsCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.clearSearchResults(); - } - } - -} -const clearSearchResultsCommand = new ClearSearchResultsCommand({ - id: SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, - kbOpts: { primary: KeyCode.Escape } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(clearSearchResultsCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class FocusNextSearchResultCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusNextResult(); - } - } - -} -const focusNextSearchResultCommand = new FocusNextSearchResultCommand({ - id: SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, - kbOpts: { primary: KeyCode.Enter } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(focusNextSearchResultCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class FocusPreviousSearchResultCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusPreviousResult(); - } - } - -} -const focusPreviousSearchResultCommand = new FocusPreviousSearchResultCommand({ - id: SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, - kbOpts: { primary: KeyMod.Shift | KeyCode.Enter } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(focusPreviousSearchResultCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - -class EditFocusedSettingCommand extends SettingsCommand { - - public runCommand(accessor: ServicesAccessor, args: any): void { - const preferencesEditor = this.getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.editFocusedPreference(); - } - } - -} -const editFocusedSettingCommand = new EditFocusedSettingCommand({ - id: SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, - kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.US_DOT } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(editFocusedSettingCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 395c614d1d2..da17ebae57b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -3,43 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TPromise } from 'vs/base/common/winjs.base'; -import * as nls from 'vs/nls'; -import { Delayer } from 'vs/base/common/async'; -import * as arrays from 'vs/base/common/arrays'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Position } from 'vs/editor/common/core/position'; -import { IAction } from 'vs/base/common/actions'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Registry } from 'vs/platform/registry/common/platform'; -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IExtensionSetting, IScoredResults } from 'vs/workbench/services/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { SettingsGroupTitleWidget, EditPreferenceWidget, SettingsHeaderWidget, DefaultSettingsHeaderWidget, FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; -import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { overrideIdentifierFromKey, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { getDomNodePagePosition } from 'vs/base/browser/dom'; -import { IssueType, ISettingsSearchIssueReporterData, ISettingSearchResult } from 'vs/platform/issue/common/issue'; -import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { ContextSubMenu } from 'vs/base/browser/contextmenu'; +import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import { IAction } from 'vs/base/common/actions'; +import { Delayer } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import * as nls from 'vs/nls'; +import { ConfigurationTarget, IConfigurationService, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; +import { DefaultSettingsHeaderWidget, EditPreferenceWidget, FloatingClickWidget, SettingsGroupTitleWidget, SettingsHeaderWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IFilterResult, IPreferencesEditorModel, IPreferencesService, IScoredResults, ISetting, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; +import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -90,7 +84,6 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend this._register(this.editSettingActionRenderer.onUpdateSetting(({ key, value, source }) => this._updatePreference(key, value, source))); this._register(this.editor.getModel().onDidChangeContent(() => this.modelChangeDelayer.trigger(() => this.onModelChanged()))); - this.createHeader(); } public getAssociatedPreferencesModel(): IPreferencesEditorModel { @@ -100,6 +93,9 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend public setAssociatedPreferencesModel(associatedPreferencesModel: IPreferencesEditorModel): void { this.associatedPreferencesModel = associatedPreferencesModel; this.editSettingActionRenderer.associatedPreferencesModel = associatedPreferencesModel; + + // Create header only in Settings editor mode + this.createHeader(); } protected createHeader(): void { @@ -239,10 +235,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private filteredMatchesRenderer: FilteredMatchesRenderer; private hiddenAreasRenderer: HiddenAreasRenderer; private editSettingActionRenderer: EditSettingRenderer; - private issueWidgetRenderer: IssueWidgetRenderer; private feedbackWidgetRenderer: FeedbackWidgetRenderer; private bracesHidingRenderer: BracesHidingRenderer; - private extensionCodelensRenderer: ExtensionCodelensRenderer; private filterResult: IFilterResult; private readonly _onUpdatePreference: Emitter<{ key: string, value: any, source: IIndexedSetting }> = new Emitter<{ key: string, value: any, source: IIndexedSetting }>(); @@ -265,11 +259,9 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsGroupTitleRenderer = this._register(instantiationService.createInstance(SettingsGroupTitleRenderer, editor)); this.filteredMatchesRenderer = this._register(instantiationService.createInstance(FilteredMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(instantiationService.createInstance(EditSettingRenderer, editor, preferencesModel, this.settingHighlighter)); - this.issueWidgetRenderer = this._register(instantiationService.createInstance(IssueWidgetRenderer, editor)); this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor)); this.bracesHidingRenderer = this._register(instantiationService.createInstance(BracesHidingRenderer, editor, preferencesModel)); this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, this.bracesHidingRenderer])); - this.extensionCodelensRenderer = this._register(instantiationService.createInstance(ExtensionCodelensRenderer, editor)); this._register(this.editSettingActionRenderer.onUpdateSetting(e => this._onUpdatePreference.fire(e))); this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render())); @@ -288,7 +280,6 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public render() { this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); - this.issueWidgetRenderer.render(null); this.feedbackWidgetRenderer.render(null); this.settingHighlighter.clear(true); this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); @@ -307,7 +298,6 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingHighlighter.clear(true); this.bracesHidingRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); - this.extensionCodelensRenderer.render(filterResult); } else { this.settingHighlighter.clear(true); this.filteredMatchesRenderer.render(null, this.preferencesModel.settingsGroups); @@ -317,7 +307,6 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsGroupTitleRenderer.showGroup(0); this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); - this.extensionCodelensRenderer.render(null); } this.hiddenAreasRenderer.render(); @@ -326,11 +315,9 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private renderIssueWidget(filterResult: IFilterResult): void { const workbenchSettings = this.configurationService.getValue().workbench.settings; if (workbenchSettings.enableNaturalLanguageSearchFeedback) { - this.issueWidgetRenderer.render(null); this.feedbackWidgetRenderer.render(filterResult); } else { this.feedbackWidgetRenderer.render(null); - this.issueWidgetRenderer.render(filterResult); } } @@ -775,88 +762,6 @@ export class FeedbackWidgetRenderer extends Disposable { } } -export class IssueWidgetRenderer extends Disposable { - private _issueWidget: FloatingClickWidget; - private _currentResult: IFilterResult; - - constructor(private editor: ICodeEditor, - @IInstantiationService private instantiationService: IInstantiationService, - @IWorkbenchIssueService private issueService: IWorkbenchIssueService, - @IEnvironmentService private environmentService: IEnvironmentService - ) { - super(); - } - - public render(result: IFilterResult): void { - this._currentResult = result; - if (result && result.metadata && this.environmentService.appQuality !== 'stable') { - this.showWidget(); - } else if (this._issueWidget) { - this.disposeWidget(); - } - } - - private showWidget(): void { - if (!this._issueWidget) { - this._issueWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('reportSettingsSearchIssue', "Report Issue"), null)); - this._register(this._issueWidget.onClick(() => this.showIssueReporter())); - this._issueWidget.render(); - } - } - - private showIssueReporter(): TPromise { - const nlpMetadata = this._currentResult.metadata['nlpResult']; - const results = nlpMetadata.scoredResults; - - const enabledExtensions = nlpMetadata.extensions; - const issueResults = Object.keys(results) - .map(key => ({ - key: key.split('##')[1], - extensionId: results[key].packageId === 'core' ? - 'core' : - this.getExtensionIdByGuid(enabledExtensions, results[key].packageId), - score: results[key].score - })) - .slice(0, 20); - - const issueReporterData: Partial = { - enabledExtensions, - issueType: IssueType.SettingsSearchIssue, - actualSearchResults: issueResults, - filterResultCount: this.getFilterResultCount(), - query: this._currentResult.query - }; - - return this.issueService.openReporter(issueReporterData); - } - - private getFilterResultCount(): number { - const filterResultGroup = arrays.first(this._currentResult.filteredGroups, group => group.id === 'filterResult'); - return filterResultGroup ? - filterResultGroup.sections[0].settings.length : - 0; - } - - private getExtensionIdByGuid(extensions: ILocalExtension[], guid: string): string { - const match = arrays.first(extensions, ext => ext.identifier.uuid === guid); - - // identifier.id includes the version, not needed here - return match && `${match.manifest.publisher}.${match.manifest.name}`; - } - - private disposeWidget(): void { - if (this._issueWidget) { - this._issueWidget.dispose(); - this._issueWidget = null; - } - } - - public dispose() { - this.disposeWidget(); - super.dispose(); - } -} - export class FilteredMatchesRenderer extends Disposable implements HiddenAreasProvider { private decorationIds: string[] = []; @@ -946,51 +851,6 @@ export class HighlightMatchesRenderer extends Disposable { } } -export class ExtensionCodelensRenderer extends Disposable implements CodeLensProvider { - private filterResult: IFilterResult; - - constructor() { - super(); - this._register(CodeLensProviderRegistry.register({ pattern: '**/settings.json' }, this)); - } - - public render(filterResult: IFilterResult): void { - this.filterResult = filterResult; - } - - public provideCodeLenses(model: ITextModel, token: CancellationToken): ICodeLensSymbol[] { - if (!this.filterResult || !this.filterResult.filteredGroups) { - return []; - } - - const newExtensionGroup = arrays.first(this.filterResult.filteredGroups, g => g.id === 'newExtensionsResult'); - if (!newExtensionGroup) { - return []; - } - - return newExtensionGroup.sections[0].settings - .filter((s: IExtensionSetting) => { - // Skip any non IExtensionSettings that somehow got in here - return s.extensionName && s.extensionPublisher; - }) - .map((s: IExtensionSetting) => { - const extId = s.extensionPublisher + '.' + s.extensionName; - return { - command: { - title: nls.localize('newExtensionLabel', "Show Extension \"{0}\"", extId), - id: 'workbench.extensions.action.showExtensionsWithId', - arguments: [extId.toLowerCase()] - }, - range: new Range(s.keyRange.startLineNumber, 1, s.keyRange.startLineNumber, 1) - }; - }); - } - - public resolveCodeLens(model: ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ICodeLensSymbol { - return codeLens; - } -} - export interface IIndexedSetting extends ISetting { index: number; groupId: string; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 69dc294f532..0f889e700b9 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import URI from 'vs/base/common/uri'; -import { $ } from 'vs/base/browser/builder'; +import { URI } from 'vs/base/common/uri'; import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -318,7 +317,7 @@ export class FolderSettingsActionItem extends BaseActionItem { } public render(container: HTMLElement): void { - this.builder = $(container); + this.element = container; this.container = container; this.labelElement = DOM.$('.action-title'); @@ -356,11 +355,11 @@ export class FolderSettingsActionItem extends BaseActionItem { } } - protected _updateEnabled(): void { + protected updateEnabled(): void { this.update(); } - protected _updateChecked(): void { + protected updateChecked(): void { this.update(); } @@ -561,6 +560,8 @@ export class SettingsTargetsWidget extends Widget { export interface SearchOptions extends IInputOptions { focusKey?: IContextKey; showResultCount?: boolean; + ariaLive?: string; + ariaLabelledBy?: string; } export class SearchWidget extends Widget { @@ -608,7 +609,10 @@ export class SearchWidget extends Widget { })); } - this.inputBox.inputElement.setAttribute('aria-live', 'assertive'); + this.inputBox.inputElement.setAttribute('aria-live', this.options.ariaLive || 'off'); + if (this.options.ariaLabelledBy) { + this.inputBox.inputElement.setAttribute('aria-labelledBy', this.options.ariaLabelledBy); + } const focusTracker = this._register(DOM.trackFocus(this.inputBox.inputElement)); this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); @@ -633,7 +637,8 @@ export class SearchWidget extends Widget { } public showMessage(message: string, count: number): void { - if (this.countElement) { + // Avoid setting the aria-label unnecessarily, the screenreader will read the count every time it's set, since it's aria-live:assertive. #50968 + if (this.countElement && message !== this.countElement.textContent) { this.countElement.textContent = message; this.inputBox.inputElement.setAttribute('aria-label', message); DOM.toggleClass(this.countElement, 'no-results', count === 0); diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 48b58b481a0..c0a71caad96 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -4,67 +4,101 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Action } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; -import { Color } from 'vs/base/common/color'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import * as collections from 'vs/base/common/collections'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITreeConfiguration } from 'vs/base/parts/tree/browser/tree'; -import { DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { collapseAll, expandAll } from 'vs/base/parts/tree/browser/treeUtils'; import 'vs/css!./media/settingsEditor2'; import { localize } from 'vs/nls'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler'; -import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { badgeBackground, badgeForeground, contrastBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditor } from 'vs/workbench/common/editor'; -import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; -import { ISettingsEditorViewState, SearchResultIdx, SearchResultModel, SettingsAccessibilityProvider, SettingsDataSource, SettingsRenderer, SettingsTreeController, SettingsTreeFilter, TreeElement } from 'vs/workbench/parts/preferences/browser/settingsTree'; -import { IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; -import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences'; +import { IEditor } from 'vs/workbench/common/editor'; +import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/parts/codeEditor/browser/suggestEnabledInput'; +import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; +import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; +import { ISettingLinkClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsDataSource, SettingsRenderer, SettingsTree, SimplePagedDataSource } from 'vs/workbench/parts/preferences/browser/settingsTree'; +import { ISettingsEditorViewState, MODIFIED_SETTING_TAG, ONLINE_SERVICES_SETTING_TAG, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { settingsTextInputBorder } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; +import { TOCRenderer, TOCTree, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService, ISearchResult, ISettingsEditorModel, SettingsEditorOptions, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; -import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; const $ = DOM.$; export class SettingsEditor2 extends BaseEditor { public static readonly ID: string = 'workbench.editor.settings2'; + private static NUM_INSTANCES: number = 0; - private defaultSettingsEditorModel: DefaultSettingsEditorModel; + private static readonly SUGGESTIONS: string[] = [ + '@modified', '@tag:usesOnlineServices' + ]; + private defaultSettingsEditorModel: Settings2EditorModel; + + private rootElement: HTMLElement; private headerContainer: HTMLElement; - private searchWidget: SearchWidget; + private searchWidget: SuggestEnabledInput; + private countElement: HTMLElement; private settingsTargetsWidget: SettingsTargetsWidget; - - private showConfiguredSettingsOnlyCheckbox: HTMLInputElement; - private savedExpandedGroups: any[]; + private toolbar: ToolBar; private settingsTreeContainer: HTMLElement; - private settingsTree: WorkbenchTree; - private treeDataSource: SettingsDataSource; + private settingsTree: Tree; + private settingsTreeRenderer: SettingsRenderer; + private settingsTreeDataSource: SimplePagedDataSource; + private tocTreeModel: TOCTreeModel; + private settingsTreeModel: SettingsTreeModel; + private noResultsMessage: HTMLElement; + + private tocTreeContainer: HTMLElement; + private tocTree: WorkbenchTree; - private delayedModifyLogging: Delayer; private delayedFilterLogging: Delayer; private localSearchDelayer: Delayer; private remoteSearchThrottle: ThrottledDelayer; - private searchInProgress: TPromise; + private searchInProgress: CancellationTokenSource; - private pendingSettingModifiedReport: { key: string, value: any }; + private delayRefreshOnLayout: Delayer; + private lastLayedoutWidth: number; - private selectedElement: TreeElement; + private settingUpdateDelayer: Delayer; + private pendingSettingUpdate: { key: string, value: any }; private viewState: ISettingsEditorViewState; - private searchResultModel: SearchResultModel; + private _searchResultModel: SearchResultModel; + + private tocRowFocused: IContextKey; + private inSettingsEditorContextKey: IContextKey; + private searchFocusContextKey: IContextKey; + + private scheduledRefreshes: Map; + + /** Don't spam warnings */ + private hasWarnedMissingSettings: boolean; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -74,255 +108,526 @@ export class SettingsEditor2 extends BaseEditor { @IInstantiationService private instantiationService: IInstantiationService, @IPreferencesSearchService private preferencesSearchService: IPreferencesSearchService, @ILogService private logService: ILogService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IStorageService private storageService: IStorageService, + @INotificationService private notificationService: INotificationService ) { super(SettingsEditor2.ID, telemetryService, themeService); - this.delayedModifyLogging = new Delayer(1000); this.delayedFilterLogging = new Delayer(1000); - this.localSearchDelayer = new Delayer(100); + this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); this.viewState = { settingsTarget: ConfigurationTarget.USER }; + this.delayRefreshOnLayout = new Delayer(100); - this._register(configurationService.onDidChangeConfiguration(() => this.refreshTreeAndMaintainFocus())); + this.settingUpdateDelayer = new Delayer(200); + + this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService); + this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService); + this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService); + + this.scheduledRefreshes = new Map(); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.source !== ConfigurationTarget.DEFAULT) { + this.onConfigUpdate(e.affectedKeys); + } + })); + } + + private get currentSettingsModel() { + return this.searchResultModel || this.settingsTreeModel; + } + + private get searchResultModel(): SearchResultModel { + return this._searchResultModel; + } + + private set searchResultModel(value: SearchResultModel) { + this._searchResultModel = value; + + DOM.toggleClass(this.rootElement, 'search-mode', !!this._searchResultModel); } createEditor(parent: HTMLElement): void { - const prefsEditorElement = DOM.append(parent, $('div', { class: 'settings-editor' })); + parent.setAttribute('tabindex', '-1'); + this.rootElement = DOM.append(parent, $('.settings-editor')); - this.createHeader(prefsEditorElement); - this.createBody(prefsEditorElement); + this.createHeader(this.rootElement); + this.createBody(this.rootElement); + this.updateStyles(); } - setInput(input: SettingsEditor2Input, options: EditorOptions, token: CancellationToken): Thenable { + setInput(input: SettingsEditor2Input, options: SettingsEditorOptions, token: CancellationToken): Thenable { + this.inSettingsEditorContextKey.set(true); return super.setInput(input, options, token) + .then(() => new Promise(process.nextTick)) // Force setInput to be async .then(() => { + if (!options) { + if (!this.viewState.settingsTarget) { + // Persist? + options = SettingsEditorOptions.create({ target: ConfigurationTarget.USER }); + } + } else if (!options.target) { + options.target = ConfigurationTarget.USER; + } + + this._setOptions(options); + this.render(token); + }) + .then(() => { + // Init TOC selection + this.updateTreeScrollSync(); }); } + setOptions(options: SettingsEditorOptions): void { + super.setOptions(options); + + this._setOptions(options); + } + + private _setOptions(options: SettingsEditorOptions): void { + if (!options) { + return; + } + + if (options.query) { + this.searchWidget.setValue(options.query); + } + + const target: SettingsTarget = options.folderUri || options.target; + this.settingsTargetsWidget.settingsTarget = target; + this.viewState.settingsTarget = target; + } + + clearInput(): void { + this.inSettingsEditorContextKey.set(false); + super.clearInput(); + } + layout(dimension: DOM.Dimension): void { - this.searchWidget.layout(dimension); - this.layoutSettingsList(dimension); + const firstEl = this.settingsTree.getFirstVisibleElement(); + const firstElTop = this.settingsTree.getRelativeTop(firstEl); + + this.layoutTrees(dimension); + + let innerWidth = dimension.width - 24 * 2; // 24px padding on left and right + let monacoWidth = (innerWidth > 1000 ? 1000 : innerWidth) - 10; + this.searchWidget.layout({ height: 20, width: monacoWidth }); + + DOM.toggleClass(this.rootElement, 'narrow', dimension.width < 600); + + // #56185 + if (dimension.width !== this.lastLayedoutWidth) { + this.lastLayedoutWidth = dimension.width; + this.delayRefreshOnLayout.trigger(() => { + this.renderTree(undefined, true).then(() => { + this.settingsTree.reveal(firstEl, firstElTop); + }); + }); + } } focus(): void { - this.searchWidget.focus(); + this.focusSearch(); + } + + focusSettings(): void { + const firstFocusable = this.settingsTree.getHTMLElement().querySelector(SettingsRenderer.CONTROL_SELECTOR); + if (firstFocusable) { + (firstFocusable).focus(); + } + } + + showContextMenu(): void { + const settingDOMElement = this.settingsTreeRenderer.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); + if (!settingDOMElement) { + return; + } + + const focusedKey = this.settingsTreeRenderer.getKeyForDOMElementInSetting(settingDOMElement); + if (!focusedKey) { + return; + } + + const elements = this.currentSettingsModel.getElementsByName(focusedKey); + if (elements && elements[0]) { + this.settingsTreeRenderer.showContextMenu(elements[0], settingDOMElement); + } + } + + focusSearch(filter?: string, selectAll = true): void { + if (filter && this.searchWidget) { + this.searchWidget.setValue(filter); + } + + this.searchWidget.focus(selectAll); + } + + clearSearchResults(): void { + this.searchWidget.setValue(''); } private createHeader(parent: HTMLElement): void { this.headerContainer = DOM.append(parent, $('.settings-header')); - const previewHeader = DOM.append(this.headerContainer, $('.settings-preview-header')); - - const previewAlert = DOM.append(previewHeader, $('span.settings-preview-warning')); - previewAlert.textContent = localize('previewWarning', "Preview"); - - const previewTextLabel = DOM.append(previewHeader, $('span.settings-preview-label')); - previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor"); - const searchContainer = DOM.append(this.headerContainer, $('.search-container')); - this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { - ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"), - placeholder: localize('SearchSettings.Placeholder', "Search settings") - })); - this._register(this.searchWidget.onDidChange(() => this.onSearchInputChanged())); - this._register(DOM.addStandardDisposableListener(this.searchWidget.domNode, 'keydown', e => { - if (e.keyCode === KeyCode.DownArrow) { - this.settingsTree.focusFirst(); - this.settingsTree.domFocus(); + + let searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings"); + this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { + triggerCharacters: ['@'], + provideResults: (query: string) => { + return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => tag + ' '); } + }, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, { + placeholderText: searchBoxLabel, + focusContextKey: this.searchFocusContextKey, + // TODO: Aria-live + })); + + this._register(attachSuggestEnabledInputBoxStyler(this.searchWidget, this.themeService, { + inputBorder: settingsTextInputBorder })); - const advancedCustomization = DOM.append(this.headerContainer, $('.settings-advanced-customization')); - const advancedCustomizationLabel = DOM.append(advancedCustomization, $('span.settings-advanced-customization-label')); - advancedCustomizationLabel.textContent = localize('advancedCustomizationLabel', "For advanced customizations open and edit") + ' '; - const openSettingsButton = this._register(new Button(advancedCustomization, { title: true, buttonBackground: null, buttonHoverBackground: null })); - this._register(attachButtonStyler(openSettingsButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: foreground - })); - openSettingsButton.label = localize('openSettingsLabel', "settings.json"); - openSettingsButton.element.classList.add('open-settings-button'); + this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget')); + this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => { + const background = colors.badgeBackground ? colors.badgeBackground.toString() : null; + const border = colors.contrastBorder ? colors.contrastBorder.toString() : null; - this._register(openSettingsButton.onDidClick(() => this.openSettingsFile())); + this.countElement.style.backgroundColor = background; + this.countElement.style.color = colors.badgeForeground.toString(); + + this.countElement.style.borderWidth = border ? '1px' : null; + this.countElement.style.borderStyle = border ? 'solid' : null; + this.countElement.style.borderColor = border; + })); + + this._register(this.searchWidget.onInputDidChange(() => this.onSearchInputChanged())); const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer)); this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER; - this.settingsTargetsWidget.onDidTargetChange(() => { - this.viewState.settingsTarget = this.settingsTargetsWidget.settingsTarget; - this.settingsTree.refresh(); - }); + this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)); this.createHeaderControls(headerControlsContainer); } + private onDidSettingsTargetChange(target: SettingsTarget): void { + this.viewState.settingsTarget = target; + + // TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value + this.onConfigUpdate(undefined, true); + } + private createHeaderControls(parent: HTMLElement): void { const headerControlsContainerRight = DOM.append(parent, $('.settings-header-controls-right')); - this.showConfiguredSettingsOnlyCheckbox = DOM.append(headerControlsContainerRight, $('input#configured-only-checkbox')); - this.showConfiguredSettingsOnlyCheckbox.type = 'checkbox'; - const showConfiguredSettingsOnlyLabel = DOM.append(headerControlsContainerRight, $('label.configured-only-label')); - showConfiguredSettingsOnlyLabel.textContent = localize('showOverriddenOnly', "Show modified only"); - showConfiguredSettingsOnlyLabel.htmlFor = 'configured-only-checkbox'; + this.toolbar = this._register(new ToolBar(headerControlsContainerRight, this.contextMenuService, { + ariaLabel: localize('settingsToolbarLabel', "Settings Editor Actions"), + actionRunner: this.actionRunner + })); - this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked())); + const actions: Action[] = [ + this.instantiationService.createInstance(FilterByTagAction, + localize('filterModifiedLabel', "Show modified settings"), + MODIFIED_SETTING_TAG, + this) + ]; + if (this.environmentService.appQuality !== 'stable') { + actions.push( + this.instantiationService.createInstance( + FilterByTagAction, + localize('filterOnlineServicesLabel', "Show settings for online services"), + ONLINE_SERVICES_SETTING_TAG, + this)); + actions.push(new Separator()); + } + actions.push(new Action('settings.openSettingsJson', localize('openSettingsJsonLabel', "Open settings.json"), undefined, undefined, () => { + return this.openSettingsFile().then(editor => { + const currentQuery = parseQuery(this.searchWidget.getValue()); + if (editor instanceof PreferencesEditor && currentQuery) { + editor.focusSearch(currentQuery.query); + } + }); + })); + + this.toolbar.setActions([], actions)(); + this.toolbar.context = { target: this.settingsTargetsWidget.settingsTarget }; } - private openSettingsFile(): TPromise { + private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void { + const elements = this.currentSettingsModel.getElementsByName(evt.targetKey); + if (elements && elements[0]) { + let sourceTop = this.settingsTree.getRelativeTop(evt.source); + if (sourceTop < 0) { + // e.g. clicked a searched element, now the search has been cleared + sourceTop = .5; + } + + this.settingsTree.reveal(elements[0], sourceTop); + + const domElements = this.settingsTreeRenderer.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); + if (domElements && domElements[0]) { + const control = domElements[0].querySelector(SettingsRenderer.CONTROL_SELECTOR); + if (control) { + (control).focus(); + } + } + } else if (!recursed) { + const p = this.triggerSearch(''); + p.then(() => { + this.searchWidget.setValue(''); + this.onDidClickSetting(evt, true); + }); + } + } + + private openSettingsFile(query?: string): TPromise { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; + const options: ISettingsEditorOptions = { query }; if (currentSettingsTarget === ConfigurationTarget.USER) { - return this.preferencesService.openGlobalSettings(); + return this.preferencesService.openGlobalSettings(true, options); } else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) { - return this.preferencesService.openWorkspaceSettings(); + return this.preferencesService.openWorkspaceSettings(true, options); } else { - return this.preferencesService.openFolderSettings(currentSettingsTarget); + return this.preferencesService.openFolderSettings(currentSettingsTarget, true, options); } } private createBody(parent: HTMLElement): void { const bodyContainer = DOM.append(parent, $('.settings-body')); - this.createList(bodyContainer); + this.noResultsMessage = DOM.append(bodyContainer, $('.no-results')); + this.noResultsMessage.innerText = localize('noResults', "No Settings Found"); + this._register(attachStylerCallback(this.themeService, { editorForeground }, colors => { + this.noResultsMessage.style.color = colors.editorForeground ? colors.editorForeground.toString() : null; + })); - if (this.environmentService.appQuality !== 'stable') { - this.createFeedbackButton(bodyContainer); - } + this.createFocusSink( + bodyContainer, + e => { + if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (this.settingsTree.getScrollPosition() > 0) { + const firstElement = this.settingsTree.getFirstVisibleElement(); + this.settingsTree.reveal(firstElement, 0.1); + return true; + } + } else { + const firstControl = this.settingsTree.getHTMLElement().querySelector(SettingsRenderer.CONTROL_SELECTOR); + if (firstControl) { + (firstControl).focus(); + } + } + + return false; + }, + 'settings list focus helper'); + + this.createSettingsTree(bodyContainer); + + this.createFocusSink( + bodyContainer, + e => { + if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (this.settingsTree.getScrollPosition() < 1) { + const lastElement = this.settingsTree.getLastVisibleElement(); + this.settingsTree.reveal(lastElement, 0.9); + return true; + } + } + + return false; + }, + 'settings list focus helper' + ); + + this.createTOC(bodyContainer); } - private createList(parent: HTMLElement): void { + private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { + const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); + listFocusSink.setAttribute('aria-label', label); + listFocusSink.tabIndex = 0; + this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { + if (e.relatedTarget && callback(e)) { + e.relatedTarget.focus(); + } + })); + + return listFocusSink; + } + + private createTOC(parent: HTMLElement): void { + this.tocTreeModel = new TOCTreeModel(this.viewState); + this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container')); + + const tocRenderer = this.instantiationService.createInstance(TOCRenderer); + + this.tocTree = this._register(this.instantiationService.createInstance(TOCTree, this.tocTreeContainer, + this.viewState, + { + renderer: tocRenderer + })); + + this._register(this.tocTree.onDidChangeFocus(e => { + const element: SettingsTreeGroupElement = e.focus; + if (this.searchResultModel) { + this.viewState.filterToCategory = element; + this.renderTree(); + } + + if (element && (!e.payload || !e.payload.fromScroll)) { + let refreshP = TPromise.wrap(null); + if (this.settingsTreeDataSource.pageTo(element.index, true)) { + refreshP = this.renderTree(); + } + + refreshP.then(() => this.settingsTree.reveal(element, 0)); + } + })); + + this._register(this.tocTree.onDidFocus(() => { + this.tocRowFocused.set(true); + })); + + this._register(this.tocTree.onDidBlur(() => { + this.tocRowFocused.set(false); + })); + } + + private createSettingsTree(parent: HTMLElement): void { this.settingsTreeContainer = DOM.append(parent, $('.settings-tree-container')); - this.treeDataSource = this.instantiationService.createInstance(SettingsDataSource, this.viewState); - const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); - this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); - this._register(renderer.onDidOpenSettings(() => this.openSettingsFile())); + this.settingsTreeRenderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); + this._register(this.settingsTreeRenderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); + this._register(this.settingsTreeRenderer.onDidOpenSettings(settingKey => { + this.openSettingsFile(settingKey); + })); + this._register(this.settingsTreeRenderer.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName))); + this._register(this.settingsTreeRenderer.onDidFocusSetting(element => { + this.settingsTree.reveal(element); + })); - const treeClass = 'settings-editor-tree'; - this.settingsTree = this.instantiationService.createInstance(WorkbenchTree, this.settingsTreeContainer, - { - dataSource: this.treeDataSource, - renderer: renderer, - controller: this.instantiationService.createInstance(SettingsTreeController), - accessibilityProvider: this.instantiationService.createInstance(SettingsAccessibilityProvider), - filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState), - styler: new DefaultTreestyler(DOM.createStyleSheet(), treeClass) - }, + this.settingsTreeDataSource = this.instantiationService.createInstance(SimplePagedDataSource, + this.instantiationService.createInstance(SettingsDataSource, this.viewState)); + + this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, + this.settingsTreeContainer, + this.viewState, { - ariaLabel: localize('treeAriaLabel', "Settings"), - showLoading: false, - indentPixels: 0, - twistiePixels: 15, - }); + renderer: this.settingsTreeRenderer, + dataSource: this.settingsTreeDataSource + })); + this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); - this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const activeBorderColor = theme.getColor(listActiveSelectionBackground); - if (activeBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`); - } + // 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'); - const inactiveBorderColor = theme.getColor(listInactiveSelectionBackground); - if (inactiveBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`); - } - })); - - this.settingsTree.getHTMLElement().classList.add(treeClass); - - attachStyler(this.themeService, { - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground - }, colors => { - this.settingsTree.style(colors); - }); - - this.settingsTree.onDidChangeFocus(e => { - this.settingsTree.setSelection([e.focus]); - if (this.selectedElement) { - this.settingsTree.refresh(this.selectedElement); - } - - if (e.focus) { - this.settingsTree.refresh(e.focus); - } - - this.selectedElement = e.focus; - }); - } - - private createFeedbackButton(parent: HTMLElement): void { - const feedbackButton = this._register(new Button(parent)); - feedbackButton.label = localize('feedbackButtonLabel', "Provide Feedback"); - feedbackButton.element.classList.add('settings-feedback-button'); - - this._register(attachButtonStyler(feedbackButton, this.themeService)); - this._register(feedbackButton.onDidClick(() => { - // Github master issue - window.open('https://go.microsoft.com/fwlink/?linkid=2000807'); + this._register(this.settingsTree.onDidScroll(() => { + this.updateTreeScrollSync(); })); } - private onShowConfiguredOnlyClicked(): void { - this.viewState.showConfiguredOnly = this.showConfiguredSettingsOnlyCheckbox.checked; - this.refreshTreeAndMaintainFocus(); - - // TODO@roblou - This is slow - if (this.viewState.showConfiguredOnly) { - this.savedExpandedGroups = this.settingsTree.getExpandedElements(); - const nav = this.settingsTree.getNavigator(); - let element; - while (element = nav.next()) { - this.settingsTree.expand(element); - } - } else if (this.savedExpandedGroups) { - const nav = this.settingsTree.getNavigator(); - let element; - while (element = nav.next()) { - this.settingsTree.collapse(element); - } - - this.settingsTree.expandAll(this.savedExpandedGroups); - this.savedExpandedGroups = null; + public notifyNoSaveNeeded(force: boolean = true) { + if (force || !this.storageService.getBoolean('hasNotifiedOfSettingsAutosave', StorageScope.GLOBAL, false)) { + this.storageService.store('hasNotifiedOfSettingsAutosave', true, StorageScope.GLOBAL); + this.notificationService.info(localize('settingsNoSaveNeeded', "Your changes are automatically saved as you edit.")); } } private onDidChangeSetting(key: string, value: any): void { - // ConfigurationService displays the error if this fails. - // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change - this.configurationService.updateValue(key, value, this.settingsTargetsWidget.settingsTarget) - .then(() => this.refreshTreeAndMaintainFocus()); + this.notifyNoSaveNeeded(false); - const reportModifiedProps = { - key, - query: this.searchWidget.getValue(), - searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(), - rawResults: this.searchResultModel && this.searchResultModel.getRawResults(), - showConfiguredOnly: this.viewState.showConfiguredOnly, - isReset: typeof value === 'undefined', - settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget - }; - - if (this.pendingSettingModifiedReport && key !== this.pendingSettingModifiedReport.key) { - this.reportModifiedSetting(reportModifiedProps); + if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) { + this.updateChangedSetting(key, value); } - this.pendingSettingModifiedReport = { key, value }; - this.delayedModifyLogging.trigger(() => this.reportModifiedSetting(reportModifiedProps)); + this.pendingSettingUpdate = { key, value }; + this.settingUpdateDelayer.trigger(() => this.updateChangedSetting(key, value)); + } + + private updateTreeScrollSync(): void { + this.settingsTreeRenderer.cancelSuggesters(); + if (this.searchResultModel) { + return; + } + + if (!this.tocTree.getInput()) { + return; + } + + this.updateTreePagingByScroll(); + + const elementToSync = this.settingsTree.getFirstVisibleElement(); + 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); + } + + this.tocTree.expand(element); + + this.tocTree.setSelection([element]); + this.tocTree.setFocus(element, { fromScroll: true }); + } + } + + private updateTreePagingByScroll(): void { + const lastVisibleElement = this.settingsTree.getLastVisibleElement(); + if (lastVisibleElement && this.settingsTreeDataSource.pageTo(lastVisibleElement.index)) { + this.renderTree(); + } + } + + private updateChangedSetting(key: string, value: any): TPromise { + // ConfigurationService displays the error if this fails. + // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change + const settingsTarget = this.settingsTargetsWidget.settingsTarget; + const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined; + const configurationTarget = (resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget); + const overrides: IConfigurationOverrides = { resource }; + + const isManualReset = value === undefined; + + // If the user is changing the value back to the default, do a 'reset' instead + const inspected = this.configurationService.inspect(key, overrides); + if (inspected.default === value) { + value = undefined; + } + + return this.configurationService.updateValue(key, value, overrides, configurationTarget) + .then(() => this.renderTree(key, isManualReset)) + .then(() => { + const reportModifiedProps = { + key, + query: this.searchWidget.getValue(), + searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(), + rawResults: this.searchResultModel && this.searchResultModel.getRawResults(), + showConfiguredOnly: this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG), + isReset: typeof value === 'undefined', + settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget + }; + + return this.reportModifiedSetting(reportModifiedProps); + }); } private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void { - this.pendingSettingModifiedReport = null; + this.pendingSettingUpdate = null; const remoteResult = props.searchResults && props.searchResults[SearchResultIdx.Remote]; const localResult = props.searchResults && props.searchResults[SearchResultIdx.Local]; @@ -377,84 +682,295 @@ export class SettingsEditor2 extends BaseEditor { } */ this.telemetryService.publicLog('settingsEditor.settingModified', data); + + const data2 = { + key: props.key, + groupId, + nlpIndex, + displayIndex, + showConfiguredOnly: props.showConfiguredOnly, + isReset: props.isReset, + target: reportedTarget + }; + + /* __GDPR__ + "settingsEditor.settingModified2" : { + "key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('settingsEditor.settingModified2', data2); } private render(token: CancellationToken): TPromise { if (this.input) { return this.input.resolve() - .then((model: DefaultSettingsEditorModel) => { + .then((model: Settings2EditorModel) => { if (token.isCancellationRequested) { return void 0; } + this._register(model.onDidChangeGroups(() => this.onConfigUpdate())); this.defaultSettingsEditorModel = model; - if (!this.settingsTree.getInput()) { - this.settingsTree.setInput(this.defaultSettingsEditorModel); - this.expandCommonlyUsedSettings(); - } + return this.onConfigUpdate(); }); } return TPromise.as(null); } - private refreshTreeAndMaintainFocus(): TPromise { - // Sort of a hack to maintain focus on the focused control across a refresh - const focusedRowItem = DOM.findParentWithClass(document.activeElement, 'setting-item'); - const focusedRowId = focusedRowItem && focusedRowItem.id; - const selection = focusedRowId && document.activeElement.tagName.toLowerCase() === 'input' ? - (document.activeElement).selectionStart : - null; + private onSearchModeToggled(): void { + DOM.removeClass(this.rootElement, 'no-toc-search'); + if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') { + DOM.toggleClass(this.rootElement, 'no-toc-search', !!this.searchResultModel); + } - return this.settingsTree.refresh().then(() => { - if (focusedRowId) { - const rowSelector = `.setting-item#${focusedRowId}`; - const inputElementToFocus: HTMLElement = this.settingsTreeContainer.querySelector(`${rowSelector} input, ${rowSelector} select, ${rowSelector} a`); - if (inputElementToFocus) { - inputElementToFocus.focus(); - if (typeof selection === 'number') { - (inputElementToFocus).setSelectionRange(selection, selection); - } - } - } + if (this.searchResultModel) { + this.settingsTreeDataSource.pageTo(Number.MAX_VALUE); + } else { + this.settingsTreeDataSource.reset(); + } + } + + private scheduleRefresh(element: HTMLElement, key = ''): void { + if (key && this.scheduledRefreshes.has(key)) { + return; + } + + if (!key) { + this.scheduledRefreshes.forEach(r => r.dispose()); + this.scheduledRefreshes.clear(); + } + + const scheduledRefreshTracker = DOM.trackFocus(element); + this.scheduledRefreshes.set(key, scheduledRefreshTracker); + scheduledRefreshTracker.onDidBlur(() => { + scheduledRefreshTracker.dispose(); + this.scheduledRefreshes.delete(key); + this.onConfigUpdate([key]); }); } + private onConfigUpdate(keys?: string[], forceRefresh = false): TPromise { + if (keys) { + return this.updateElementsByKey(keys); + } + + const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed + const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core'); + const settingsResult = resolveSettingsTree(tocData, dividedGroups.core); + const resolvedSettingsRoot = settingsResult.tree; + + // Warn for settings not included in layout + if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) { + let settingKeyList = []; + settingsResult.leftoverSettings.forEach(s => { + settingKeyList.push(s.key); + }); + + this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`); + this.hasWarnedMissingSettings = true; + } + + const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core); + resolvedSettingsRoot.children.unshift(commonlyUsed.tree); + + resolvedSettingsRoot.children.push(resolveExtensionsSettings(dividedGroups.extension || [])); + + if (this.searchResultModel) { + this.searchResultModel.updateChildren(); + } + + if (this.settingsTreeModel) { + this.settingsTreeModel.update(resolvedSettingsRoot); + + return this.renderTree(undefined, forceRefresh); + } else { + this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState); + this.settingsTreeModel.update(resolvedSettingsRoot); + this.settingsTree.setInput(this.settingsTreeModel.root); + + this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; + if (this.tocTree.getInput()) { + this.tocTree.refresh(); + } else { + this.tocTree.setInput(this.tocTreeModel); + } + } + + return TPromise.wrap(null); + } + + private updateElementsByKey(keys: string[]): TPromise { + if (keys.length) { + if (this.searchResultModel) { + keys.forEach(key => this.searchResultModel.updateElementsByName(key)); + } + + if (this.settingsTreeModel) { + keys.forEach(key => this.settingsTreeModel.updateElementsByName(key)); + } + + return TPromise.join( + keys.map(key => this.renderTree(key))) + .then(() => { }); + } else { + return this.renderTree(); + } + } + + private getActiveElementInSettingsTree(): HTMLElement | null { + return (document.activeElement && DOM.isAncestor(document.activeElement, this.settingsTree.getHTMLElement())) ? + document.activeElement : + null; + } + + private renderTree(key?: string, force = false): TPromise { + if (!force && key && this.scheduledRefreshes.has(key)) { + this.updateModifiedLabelForKey(key); + return TPromise.wrap(null); + } + + // If a setting control is currently focused, schedule a refresh for later + const focusedSetting = this.settingsTreeRenderer.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); + if (focusedSetting && !force) { + // If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting + if (key) { + const focusedKey = focusedSetting.getAttribute(SettingsRenderer.SETTING_KEY_ATTR); + if (focusedKey === key && + !DOM.hasClass(focusedSetting, 'setting-item-exclude')) { // update `exclude`s live, as they have a separate "submit edit" step built in before this + + this.updateModifiedLabelForKey(key); + this.scheduleRefresh(focusedSetting, key); + return TPromise.wrap(null); + } + } else { + this.scheduleRefresh(focusedSetting); + return TPromise.wrap(null); + } + } + + let refreshP: TPromise; + if (key) { + const elements = this.currentSettingsModel.getElementsByName(key); + if (elements && elements.length) { + // TODO https://github.com/Microsoft/vscode/issues/57360 + // refreshP = TPromise.join(elements.map(e => this.settingsTree.refresh(e))); + refreshP = this.settingsTree.refresh(); + } else { + // Refresh requested for a key that we don't know about + return TPromise.wrap(null); + } + } else { + refreshP = this.settingsTree.refresh(); + } + + return refreshP.then(() => { + this.tocTreeModel.update(); + this.renderResultCountMessages(); + + // if (this.searchResultModel) { + // expandAll(this.tocTree); + // } + + return this.tocTree.refresh(); + }).then(() => { }); + } + + private updateModifiedLabelForKey(key: string): void { + const dataElements = this.currentSettingsModel.getElementsByName(key); + const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not + const elements = this.settingsTreeRenderer.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key); + if (elements && elements[0]) { + DOM.toggleClass(elements[0], 'is-configured', isModified); + } + } + private onSearchInputChanged(): void { const query = this.searchWidget.getValue().trim(); this.delayedFilterLogging.cancel(); - this.triggerSearch(query).then(() => { + this.triggerSearch(query.replace(/›/g, ' ')).then(() => { if (query && this.searchResultModel) { this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults())); } }); } + private parseSettingFromJSON(query: string): string { + const match = query.match(/"([a-zA-Z.]+)": /); + return match && match[1]; + } + private triggerSearch(query: string): TPromise { + this.viewState.tagFilters = new Set(); if (query) { - return this.searchInProgress = TPromise.join([ - this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)), - this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500) - ]).then(() => { - this.searchInProgress = null; - }); + const parsedQuery = parseQuery(query); + query = parsedQuery.query; + parsedQuery.tags.forEach(tag => this.viewState.tagFilters.add(tag)); + } + + if (query && query !== '@') { + query = this.parseSettingFromJSON(query) || query; + return this.triggerFilterPreferences(query); } else { - this.localSearchDelayer.cancel(); - this.remoteSearchThrottle.cancel(); - if (this.searchInProgress && this.searchInProgress.cancel) { - this.searchInProgress.cancel(); + if (this.viewState.tagFilters && this.viewState.tagFilters.size) { + this.searchResultModel = this.createFilterModel(); + } else { + this.searchResultModel = null; } - this.searchResultModel = null; - this.settingsTree.setInput(this.defaultSettingsEditorModel); - this.expandCommonlyUsedSettings(); + this.localSearchDelayer.cancel(); + this.remoteSearchThrottle.cancel(); + if (this.searchInProgress) { + this.searchInProgress.cancel(); + this.searchInProgress.dispose(); + this.searchInProgress = null; + } - return TPromise.wrap(null); + this.viewState.filterToCategory = null; + this.tocTreeModel.currentSearchModel = this.searchResultModel; + this.tocTree.refresh(); + this.onSearchModeToggled(); + + if (this.searchResultModel) { + // Added a filter model + this.tocTree.setSelection([]); + expandAll(this.tocTree); + return this.settingsTree.setInput(this.searchResultModel.root).then(() => { + this.renderResultCountMessages(); + }); + } else { + // Leaving search mode + collapseAll(this.tocTree); + return this.settingsTree.setInput(this.settingsTreeModel.root).then(() => this.renderResultCountMessages()); + } } } - private expandCommonlyUsedSettings(): void { - const commonlyUsedGroup = this.defaultSettingsEditorModel.settingsGroups[0]; - this.settingsTree.expand(this.treeDataSource.getGroupElement(commonlyUsedGroup, 0)); + /** + * Return a fake SearchResultModel which can hold a flat list of all settings, to be filtered (@modified etc) + */ + private createFilterModel(): SearchResultModel { + const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState); + + const fullResult: ISearchResult = { + filterMatches: [] + }; + for (let g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) { + for (let sect of g.sections) { + for (let setting of sect.settings) { + fullResult.filterMatches.push({ setting, matches: [], score: 0 }); + } + } + } + + filterModel.setResult(0, fullResult); + + return filterModel; } private reportFilteringUsed(query: string, results: ISearchResult[]): void { @@ -467,7 +983,10 @@ export class SettingsEditor2 extends BaseEditor { // Count unique results const counts = {}; const filterResult = results[SearchResultIdx.Local]; - counts['filterResult'] = filterResult.filterMatches.length; + if (filterResult) { + counts['filterResult'] = filterResult.filterMatches.length; + } + if (nlpResult) { counts['nlpResult'] = nlpResult.filterMatches.length; } @@ -491,45 +1010,112 @@ export class SettingsEditor2 extends BaseEditor { } */ this.telemetryService.publicLog('settingsEditor.filter', data); + + const data2 = { + durations, + counts, + requestCount + }; + + /* __GDPR__ + "settingsEditor.filter2" : { + "durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('settingsEditor.filter2', data2); } - private localFilterPreferences(query: string): TPromise { - const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); - return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider); - } + private triggerFilterPreferences(query: string): TPromise { + if (this.searchInProgress) { + this.searchInProgress.cancel(); + this.searchInProgress = null; + } - private remoteSearchPreferences(query: string): TPromise { - const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query); - return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider); - } - - private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider: ISearchProvider): TPromise { - const filterPs: TPromise[] = [this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider)]; - - let isCanceled = false; - return new TPromise(resolve => { - return TPromise.join(filterPs).then(results => { - if (isCanceled) { - // Handle cancellation like this because cancellation is lost inside the search provider due to async/await - return null; - } - - const [result] = results; - if (!this.searchResultModel) { - this.searchResultModel = new SearchResultModel(); - this.settingsTree.setInput(this.searchResultModel); - } - - this.searchResultModel.setResult(type, result); - resolve(this.refreshTreeAndMaintainFocus()); - }); - }, () => { - isCanceled = true; + // Trigger the local search. If it didn't find an exact match, trigger the remote search. + const searchInProgress = this.searchInProgress = new CancellationTokenSource(); + return this.localSearchDelayer.trigger(() => { + if (searchInProgress && !searchInProgress.token.isCancellationRequested) { + return this.localFilterPreferences(query).then(result => { + if (!result.exactMatch) { + this.remoteSearchThrottle.trigger(() => { + return searchInProgress && !searchInProgress.token.isCancellationRequested ? + this.remoteSearchPreferences(query, this.searchInProgress.token) : + TPromise.wrap(null); + }); + } + }); + } else { + return TPromise.wrap(null); + } }); } - private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider: ISearchProvider): TPromise { - const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null); + private localFilterPreferences(query: string, token?: CancellationToken): TPromise { + const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); + return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider, token); + } + + private remoteSearchPreferences(query: string, token?: CancellationToken): TPromise { + const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query); + const newExtSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query, true); + + return TPromise.join([ + this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider, token), + this.filterOrSearchPreferences(query, SearchResultIdx.NewExtensions, newExtSearchProvider, token) + ]).then(() => { + this.renderResultCountMessages(); + }); + } + + private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider: ISearchProvider, token?: CancellationToken): TPromise { + return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token).then(result => { + if (token && token.isCancellationRequested) { + // Handle cancellation like this because cancellation is lost inside the search provider due to async/await + return null; + } + + if (!this.searchResultModel) { + this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState); + this.searchResultModel.setResult(type, result); + this.tocTreeModel.currentSearchModel = this.searchResultModel; + this.onSearchModeToggled(); + this.settingsTree.setInput(this.searchResultModel.root); + } else { + this.searchResultModel.setResult(type, result); + this.tocTreeModel.update(); + } + + this.tocTree.setSelection([]); + this.viewState.filterToCategory = null; + expandAll(this.tocTree); + + return this.renderTree().then(() => result); + }); + } + + private renderResultCountMessages() { + if (!this.settingsTree.getInput()) { + return; + } + + if (this.tocTreeModel && this.tocTreeModel.settingsTreeRoot) { + const count = this.tocTreeModel.settingsTreeRoot.count; + switch (count) { + case 0: this.countElement.innerText = localize('noResults', "No Settings Found"); break; + case 1: this.countElement.innerText = localize('oneResult', "1 Setting Found"); break; + default: this.countElement.innerText = localize('moreThanOneResult', "{0} Settings Found", count); + } + + this.countElement.style.display = 'block'; + this.noResultsMessage.style.display = count === 0 ? 'block' : 'none'; + } + } + + private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider: ISearchProvider, token?: CancellationToken): TPromise { + const searchP = provider ? provider.searchModel(model, token) : TPromise.wrap(null); return searchP .then(null, err => { if (isPromiseCanceledError(err)) { @@ -552,9 +1138,37 @@ export class SettingsEditor2 extends BaseEditor { }); } - private layoutSettingsList(dimension: DOM.Dimension): void { - const listHeight = dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/); - this.settingsTreeContainer.style.height = `${listHeight}px`; - this.settingsTree.layout(listHeight, 800); + private layoutTrees(dimension: DOM.Dimension): void { + const listHeight = dimension.height - (76 + 11 /* header height + padding*/); + const settingsTreeHeight = listHeight - 14; + this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`; + this.settingsTree.layout(settingsTreeHeight, 800); + + const tocTreeHeight = listHeight - 16; + this.tocTreeContainer.style.height = `${tocTreeHeight}px`; + this.tocTree.layout(tocTreeHeight, 175); + + this.settingsTreeRenderer.updateWidth(dimension.width); } -} \ No newline at end of file +} + +interface ISettingsToolbarContext { + target: SettingsTarget; +} + +class FilterByTagAction extends Action { + static readonly ID = 'settings.filterByTag'; + + constructor( + label: string, + private tag: string, + private settingsEditor: SettingsEditor2 + ) { + super(FilterByTagAction.ID, label, 'toggle-filter-tag'); + } + + run(): TPromise { + this.settingsEditor.focusSearch(this.tag === MODIFIED_SETTING_TAG ? `@${this.tag} ` : `@tag:${this.tag} `, false); + return TPromise.as(null); + } +} diff --git a/src/vs/workbench/parts/preferences/browser/settingsLayout.ts b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts new file mode 100644 index 00000000000..e98adfcb7bb --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/settingsLayout.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { ISetting } from 'vs/workbench/services/preferences/common/preferences'; + +export interface ITOCEntry { + id: string; + label: string; + + children?: ITOCEntry[]; + settings?: (string | ISetting)[]; +} + +export const commonlyUsedData: ITOCEntry = { + id: 'commonlyUsed', + label: localize('commonlyUsed', "Commonly Used"), + settings: ['files.autoSave', 'editor.fontSize', 'editor.fontFamily', 'editor.tabSize', 'editor.renderWhitespace', 'editor.cursorStyle', 'editor.multiCursorModifier', 'editor.insertSpaces', 'editor.wordWrap', 'files.exclude', 'files.associations'] +}; + +export const tocData: ITOCEntry = { + id: 'root', + label: 'root', + children: [ + { + id: 'editor', + label: localize('textEditor', "Text Editor"), + settings: ['editor.*'], + children: [ + { + id: 'editor/cursor', + label: localize('cursor', "Cursor"), + settings: ['editor.cursor*'] + }, + { + id: 'editor/find', + label: localize('find', "Find"), + settings: ['editor.find.*'] + }, + { + id: 'editor/font', + label: localize('font', "Font"), + settings: ['editor.font*'] + }, + { + id: 'editor/format', + label: localize('formatting', "Formatting"), + settings: ['editor.format*'] + }, + { + id: 'editor/diffEditor', + label: localize('diffEditor', "Diff Editor"), + settings: ['diffEditor.*'] + }, + { + id: 'editor/minimap', + label: localize('minimap', "Minimap"), + settings: ['editor.minimap.*'] + }, + { + id: 'editor/suggestions', + label: localize('suggestions', "Suggestions"), + settings: ['editor.*suggest*'] + }, + { + id: 'editor/files', + label: localize('files', "Files"), + settings: ['files.*'] + } + ] + }, + { + id: 'workbench', + label: localize('workbench', "Workbench"), + settings: ['workbench.*'], + children: [ + { + id: 'workbench/appearance', + label: localize('appearance', "Appearance"), + settings: ['workbench.activityBar.*', 'workbench.*color*', 'workbench.fontAliasing', 'workbench.iconTheme', 'workbench.sidebar.location', 'workbench.*.visible', 'workbench.tips.enabled', 'workbench.tree.*', 'workbench.view.*'] + }, + { + id: 'workbench/breadcrumbs', + label: localize('breadcrumbs', "Breadcrumbs"), + settings: ['breadcrumbs.*'] + }, + { + id: 'workbench/editor', + label: localize('editorManagement', "Editor Management"), + settings: ['workbench.editor.*'] + }, + { + id: 'workbench/settings', + label: localize('settings', "Settings Editor"), + settings: ['workbench.settings.*'] + }, + { + id: 'workbench/zenmode', + label: localize('zenMode', "Zen Mode"), + settings: ['zenmode.*'] + } + ] + }, + { + id: 'window', + label: localize('window', "Window"), + settings: ['window.*'], + children: [ + { + id: 'window/newWindow', + label: localize('newWindow', "New Window"), + settings: ['window.*newwindow*'] + } + ] + }, + { + id: 'features', + label: localize('features', "Features"), + children: [ + { + id: 'features/explorer', + label: localize('fileExplorer', "Explorer"), + settings: ['explorer.*', 'outline.*'] + }, + { + id: 'features/search', + label: localize('search', "Search"), + settings: ['search.*'] + } + , + { + id: 'features/debug', + label: localize('debug', "Debug"), + settings: ['debug.*', 'launch'] + }, + { + id: 'features/scm', + label: localize('scm', "SCM"), + settings: ['scm.*'] + }, + { + id: 'features/extensions', + label: localize('extensionViewlet', "Extension Viewlet"), + settings: ['extensions.*'] + }, + { + id: 'features/terminal', + label: localize('terminal', "Terminal"), + settings: ['terminal.*'] + }, + { + id: 'features/problems', + label: localize('problems', "Problems"), + settings: ['problems.*'] + } + ] + }, + { + id: 'application', + label: localize('application', "Application"), + children: [ + { + id: 'application/http', + label: localize('proxy', "Proxy"), + settings: ['http.*'] + }, + { + id: 'application/keyboard', + label: localize('keyboard', "Keyboard"), + settings: ['keyboard.*'] + }, + { + id: 'application/update', + label: localize('update', "Update"), + settings: ['update.*'] + }, + { + id: 'application/telemetry', + label: localize('telemetry', "Telemetry"), + settings: ['telemetry.*'] + } + ] + } + ] +}; + +export const knownAcronyms = new Set(); +[ + 'css', + 'html', + 'scss', + 'less', + 'json', + 'js', + 'ts', + 'ie', + 'id', + 'php', +].forEach(str => knownAcronyms.add(str)); diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index ae7b0790c4a..c9031217e2b 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -4,251 +4,328 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; +import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; -import { Color } from 'vs/base/common/color'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Action, IAction } from 'vs/base/common/actions'; +import * as arrays from 'vs/base/common/arrays'; +import { Color, RGBA } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import * as objects from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IAccessibilityProvider, IDataSource, IFilter, IRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; +import { IAccessibilityProvider, IDataSource, IFilter, IRenderer as ITreeRenderer, ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree'; +import { DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { localize } from 'vs/nls'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { editorActiveLinkForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; -import { ISearchResult, ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; -import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { ITOCEntry } from 'vs/workbench/parts/preferences/browser/settingsLayout'; +import { ISettingsEditorViewState, isExcludeSetting, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { ExcludeSettingWidget, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; +import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; const $ = DOM.$; -export const modifiedItemForeground = registerColor('settings.modifiedItemForeground', { - light: '#019001', - dark: '#73C991', - hc: '#73C991' -}, localize('modifiedItemForeground', "(For settings editor preview) The foreground color for a modified setting.")); +function getExcludeDisplayValue(element: SettingsTreeSettingElement): IExcludeDataItem[] { + const data = element.isConfigured ? + { ...element.defaultValue, ...element.scopeValue } : + element.defaultValue; -registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const modifiedItemForegroundColor = theme.getColor(modifiedItemForeground); - if (modifiedItemForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-is-configured-label { color: ${modifiedItemForegroundColor}; }`); + return Object.keys(data) + .filter(key => !!data[key]) + .map(key => { + const value = data[key]; + const sibling = typeof value === 'boolean' ? undefined : value.when; + + return { + id: key, + pattern: key, + sibling + }; + }); +} + +export function resolveSettingsTree(tocData: ITOCEntry, coreSettingsGroups: ISettingsGroup[]): { tree: ITOCEntry, leftoverSettings: Set } { + const allSettings = getFlatSettings(coreSettingsGroups); + return { + tree: _resolveSettingsTree(tocData, allSettings), + leftoverSettings: allSettings + }; +} + +export function resolveExtensionsSettings(groups: ISettingsGroup[]): ITOCEntry { + const settingsGroupToEntry = (group: ISettingsGroup) => { + const flatSettings = arrays.flatten( + group.sections.map(section => section.settings)); + + return { + id: group.id, + label: group.title, + settings: flatSettings + }; + }; + + const extGroups = groups + .sort((a, b) => a.title.localeCompare(b.title)) + .map(g => settingsGroupToEntry(g)); + + return { + id: 'extensions', + label: localize('extensions', "Extensions"), + children: extGroups + }; +} + +function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set): ITOCEntry { + let children: ITOCEntry[]; + if (tocData.children) { + children = tocData.children + .map(child => _resolveSettingsTree(child, allSettings)) + .filter(child => (child.children && child.children.length) || (child.settings && child.settings.length)); } -}); -export interface ITreeItem { - id: string; + let settings: ISetting[]; + if (tocData.settings) { + settings = arrays.flatten(tocData.settings.map(pattern => getMatchingSettings(allSettings, pattern))); + } + + if (!children && !settings) { + return null; + } + + return { + id: tocData.id, + label: tocData.label, + children, + settings + }; } -export enum TreeItemType { - setting, - groupTitle +function getMatchingSettings(allSettings: Set, pattern: string): ISetting[] { + const result: ISetting[] = []; + + allSettings.forEach(s => { + if (settingMatches(s, pattern)) { + result.push(s); + allSettings.delete(s); + } + }); + + + return result.sort((a, b) => a.key.localeCompare(b.key)); } -export interface ISettingElement extends ITreeItem { - type: TreeItemType.setting; - parent: ISettingsGroup; - setting: ISetting; +const settingPatternCache = new Map(); - displayCategory: string; - displayLabel: string; - value: any; - isConfigured: boolean; - overriddenScopeList: string[]; - description: string; - valueType?: string | string[]; - enum?: string[]; +function createSettingMatchRegExp(pattern: string): RegExp { + pattern = escapeRegExpCharacters(pattern) + .replace(/\\\*/g, '.*'); + + return new RegExp(`^${pattern}`, 'i'); } -export interface IGroupElement extends ITreeItem { - type: TreeItemType.groupTitle; - parent: DefaultSettingsEditorModel; - group: ISettingsGroup; - index: number; +function settingMatches(s: ISetting, pattern: string): boolean { + let regExp = settingPatternCache.get(pattern); + if (!regExp) { + regExp = createSettingMatchRegExp(pattern); + settingPatternCache.set(pattern, regExp); + } + + return regExp.test(s.key); } -export type TreeElement = ISettingElement | IGroupElement; -export type TreeElementOrRoot = TreeElement | DefaultSettingsEditorModel | SearchResultModel; +function getFlatSettings(settingsGroups: ISettingsGroup[]) { + const result: Set = new Set(); -function inspectSetting(key: string, target: SettingsTarget, configurationService: IConfigurationService): { isConfigured: boolean, inspected: any, targetSelector: string } { - const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; - const inspected = configurationService.inspect(key, inspectOverrides); - const targetSelector = target === ConfigurationTarget.USER ? 'user' : - target === ConfigurationTarget.WORKSPACE ? 'workspace' : - 'workspaceFolder'; - const isConfigured = typeof inspected[targetSelector] !== 'undefined'; + for (let group of settingsGroups) { + for (let section of group.sections) { + for (let s of section.settings) { + if (!s.overrides || !s.overrides.length) { + result.add(s); + } + } + } + } - return { isConfigured, inspected, targetSelector }; + return result; } + export class SettingsDataSource implements IDataSource { - constructor( - private viewState: ISettingsEditorViewState, - @IConfigurationService private configurationService: IConfigurationService - ) { } - getGroupElement(group: ISettingsGroup, index: number): IGroupElement { - return { - type: TreeItemType.groupTitle, - group, - id: `${group.title}_${group.id}`, - index - }; + getId(tree: ITree, element: SettingsTreeElement): string { + return element.id; } - getSettingElement(setting: ISetting, group: ISettingsGroup): ISettingElement { - const { isConfigured, inspected, targetSelector } = inspectSetting(setting.key, this.viewState.settingsTarget, this.configurationService); - - const displayValue = isConfigured ? inspected[targetSelector] : inspected.default; - const overriddenScopeList = []; - if (targetSelector === 'user' && typeof inspected.workspace !== 'undefined') { - overriddenScopeList.push(localize('workspace', "Workspace")); - } - - if (targetSelector === 'workspace' && typeof inspected.user !== 'undefined') { - overriddenScopeList.push(localize('user', "User")); - } - - const displayKeyFormat = settingKeyToDisplayFormat(setting.key); - return { - type: TreeItemType.setting, - parent: group, - id: `${group.id}_${setting.key.replace(/\./g, '_')}`, - setting, - - displayLabel: displayKeyFormat.label, - displayCategory: displayKeyFormat.category, - isExpanded: false, - - value: displayValue, - isConfigured, - overriddenScopeList, - description: setting.description.join('\n'), - enum: setting.enum, - valueType: setting.type - }; - } - - getId(tree: ITree, element: TreeElementOrRoot): string { - return element instanceof DefaultSettingsEditorModel ? 'root' : element.id; - } - - hasChildren(tree: ITree, element: TreeElementOrRoot): boolean { - if (element instanceof DefaultSettingsEditorModel) { - return true; - } - - if (element instanceof SearchResultModel) { - return true; - } - - if (element.type === TreeItemType.groupTitle) { + hasChildren(tree: ITree, element: SettingsTreeElement): boolean { + if (element instanceof SettingsTreeGroupElement) { return true; } return false; } - _getChildren(element: TreeElementOrRoot): TreeElement[] { - if (element instanceof DefaultSettingsEditorModel) { - return this.getRootChildren(element); - } else if (element instanceof SearchResultModel) { - return this.getGroupChildren(element.resultsAsGroup()); - } else if (element.type === TreeItemType.groupTitle) { - return this.getGroupChildren(element.group); + getChildren(tree: ITree, element: SettingsTreeElement): TPromise { + return TPromise.as(this._getChildren(element)); + } + + private _getChildren(element: SettingsTreeElement): SettingsTreeElement[] { + if (element instanceof SettingsTreeGroupElement) { + return element.children; } else { // No children... return null; } } - getChildren(tree: ITree, element: TreeElementOrRoot): TPromise { - return TPromise.as(this._getChildren(element)); + getParent(tree: ITree, element: SettingsTreeElement): TPromise { + return TPromise.wrap(element && element.parent); } - private getRootChildren(root: DefaultSettingsEditorModel): TreeElement[] { - return root.settingsGroups - .map((g, i) => this.getGroupElement(g, i)); - } - - private getGroupChildren(group: ISettingsGroup): ISettingElement[] { - const entries: ISettingElement[] = []; - for (const section of group.sections) { - for (const setting of section.settings) { - entries.push(this.getSettingElement(setting, group)); - } - } - - return entries; - } - - getParent(tree: ITree, element: TreeElement): TPromise { - if (!element) { - return null; - } - - if (!(element instanceof DefaultSettingsEditorModel)) { - return TPromise.wrap(element.parent); - } - - return TPromise.wrap(null); + shouldAutoexpand(): boolean { + return true; } } -export function settingKeyToDisplayFormat(key: string): { category: string, label: string } { - let label = key - .replace(/\.([a-z])/g, (match, p1) => `.${p1.toUpperCase()}`) - .replace(/([a-z])([A-Z])/g, '$1 $2') // fooBar => foo Bar - .replace(/^[a-z]/g, match => match.toUpperCase()); // foo => Foo +export class SimplePagedDataSource implements IDataSource { + private static readonly SETTINGS_PER_PAGE = 30; + private static readonly BUFFER = 5; - const lastDotIdx = label.lastIndexOf('.'); - let category = ''; - if (lastDotIdx >= 0) { - category = label.substr(0, lastDotIdx); - label = label.substr(lastDotIdx + 1); + private loadedToIndex: number; + + constructor(private realDataSource: IDataSource) { + this.reset(); } - return { category, label }; + reset(): void { + this.loadedToIndex = SimplePagedDataSource.SETTINGS_PER_PAGE; + } + + pageTo(index: number, top = false): boolean { + const buffer = top ? SimplePagedDataSource.SETTINGS_PER_PAGE : SimplePagedDataSource.BUFFER; + + if (index > this.loadedToIndex - buffer) { + this.loadedToIndex = (Math.ceil(index / SimplePagedDataSource.SETTINGS_PER_PAGE) + 1) * SimplePagedDataSource.SETTINGS_PER_PAGE; + return true; + } else { + return false; + } + } + + getId(tree: ITree, element: any): string { + return this.realDataSource.getId(tree, element); + } + + hasChildren(tree: ITree, element: any): boolean { + return this.realDataSource.hasChildren(tree, element); + } + + getChildren(tree: ITree, element: SettingsTreeGroupElement): TPromise { + return this.realDataSource.getChildren(tree, element).then(realChildren => { + return this._getChildren(realChildren); + }); + } + + _getChildren(realChildren: SettingsTreeElement[]): any[] { + const lastChild = realChildren[realChildren.length - 1]; + if (lastChild && lastChild.index > this.loadedToIndex) { + return realChildren.filter(child => { + return child.index < this.loadedToIndex; + }); + } else { + return realChildren; + } + } + + getParent(tree: ITree, element: any): TPromise { + return this.realDataSource.getParent(tree, element); + } + + shouldAutoexpand(tree: ITree, element: any): boolean { + return this.realDataSource.shouldAutoexpand(tree, element); + } } -export interface ISettingsEditorViewState { - settingsTarget: SettingsTarget; - showConfiguredOnly?: boolean; -} - -export interface IDisposableTemplate { +interface IDisposableTemplate { toDispose: IDisposable[]; } -export interface ISettingItemTemplate extends IDisposableTemplate { - parent: HTMLElement; +interface ISettingItemTemplate extends IDisposableTemplate { + onChange?: (value: T) => void; - context?: ISettingElement; + context?: SettingsTreeSettingElement; containerElement: HTMLElement; categoryElement: HTMLElement; labelElement: HTMLElement; descriptionElement: HTMLElement; - expandIndicatorElement: HTMLElement; - valueElement: HTMLElement; - isConfiguredElement: HTMLElement; + controlElement: HTMLElement; + deprecationWarningElement: HTMLElement; otherOverridesElement: HTMLElement; + toolbar: ToolBar; } -export interface IGroupTitleTemplate extends IDisposableTemplate { - context?: IGroupElement; +interface ISettingBoolItemTemplate extends ISettingItemTemplate { + checkbox: Checkbox; +} + +interface ISettingTextItemTemplate extends ISettingItemTemplate { + inputBox: InputBox; + validationErrorMessageElement: HTMLElement; +} + +type ISettingNumberItemTemplate = ISettingTextItemTemplate; + +interface ISettingEnumItemTemplate extends ISettingItemTemplate { + selectBox: SelectBox; + enumDescriptionElement: HTMLElement; +} + +interface ISettingComplexItemTemplate extends ISettingItemTemplate { + button: Button; +} + +interface ISettingExcludeItemTemplate extends ISettingItemTemplate { + excludeWidget: ExcludeSettingWidget; +} + +interface ISettingNewExtensionsTemplate extends IDisposableTemplate { + button: Button; + context?: SettingsTreeNewExtensionsElement; +} + +interface IGroupTitleTemplate extends IDisposableTemplate { + context?: SettingsTreeGroupElement; parent: HTMLElement; - labelElement: HTMLElement; } -const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.entry.template'; +const SETTINGS_TEXT_TEMPLATE_ID = 'settings.text.template'; +const SETTINGS_NUMBER_TEMPLATE_ID = 'settings.number.template'; +const SETTINGS_ENUM_TEMPLATE_ID = 'settings.enum.template'; +const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template'; +const SETTINGS_EXCLUDE_TEMPLATE_ID = 'settings.exclude.template'; +const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template'; +const SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID = 'settings.newExtensions.template'; const SETTINGS_GROUP_ELEMENT_TEMPLATE_ID = 'settings.group.template'; export interface ISettingChangeEvent { @@ -256,61 +333,213 @@ export interface ISettingChangeEvent { value: any; // undefined => reset/unconfigure } -export class SettingsRenderer implements IRenderer { +export interface ISettingLinkClickEvent { + source: SettingsTreeSettingElement; + targetKey: string; +} - private static readonly SETTING_ROW_HEIGHT = 75; +export class SettingsRenderer implements ITreeRenderer { + + public static readonly CONTROL_CLASS = 'setting-control-focus-target'; + public static readonly CONTROL_SELECTOR = '.' + SettingsRenderer.CONTROL_CLASS; + + public static readonly SETTING_KEY_ATTR = 'data-key'; private readonly _onDidChangeSetting: Emitter = new Emitter(); public readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; - private readonly _onDidOpenSettings: Emitter = new Emitter(); - public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; + private readonly _onDidOpenSettings: Emitter = new Emitter(); + public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; - private measureContainer: HTMLElement; + private readonly _onDidClickSettingLink: Emitter = new Emitter(); + public readonly onDidClickSettingLink: Event = this._onDidClickSettingLink.event; + + private readonly _onDidFocusSetting: Emitter = new Emitter(); + public readonly onDidFocusSetting: Event = this._onDidFocusSetting.event; + + private descriptionMeasureContainer: HTMLElement; + private longestSingleLineDescription = 0; + + private rowHeightCache = new Map(); + private lastRenderedWidth: number; + + private settingActions: IAction[]; constructor( _measureContainer: HTMLElement, @IThemeService private themeService: IThemeService, - @IContextViewService private contextViewService: IContextViewService + @IContextViewService private contextViewService: IContextViewService, + @IOpenerService private readonly openerService: IOpenerService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IKeybindingService private keybindingService: IKeybindingService, ) { - this.measureContainer = DOM.append(_measureContainer, $('.setting-measure-container.monaco-tree-row')); + this.descriptionMeasureContainer = $('.setting-item-description'); + DOM.append(_measureContainer, + $('.setting-measure-container.monaco-tree-row', undefined, + $('.setting-item', undefined, + this.descriptionMeasureContainer))); + + this.settingActions = [ + new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, (context: SettingsTreeSettingElement) => { + if (context) { + this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined }); + } + + return TPromise.wrap(null); + }), + new Separator(), + this.instantiationService.createInstance(CopySettingIdAction), + this.instantiationService.createInstance(CopySettingAsJSONAction), + ]; } - getHeight(tree: ITree, element: TreeElement): number { - if (element.type === TreeItemType.groupTitle) { - return 30; + showContextMenu(element: SettingsTreeSettingElement, settingDOMElement: HTMLElement): void { + const toolbarElement: HTMLElement = settingDOMElement.querySelector('.toolbar-toggle-more'); + if (toolbarElement) { + this.contextMenuService.showContextMenu({ + getActions: () => TPromise.wrap(this.settingActions), + getAnchor: () => toolbarElement, + getActionsContext: () => element + }); + } + } + + updateWidth(width: number): void { + if (this.lastRenderedWidth !== width) { + this.rowHeightCache = new Map(); + } + this.longestSingleLineDescription = 0; + + this.lastRenderedWidth = width; + } + + getHeight(tree: ITree, element: SettingsTreeElement): number { + if (this.rowHeightCache.has(element.id) && !(element instanceof SettingsTreeSettingElement && isExcludeSetting(element.setting))) { + return this.rowHeightCache.get(element.id); } - if (element.type === TreeItemType.setting) { - const isSelected = this.elementIsSelected(tree, element); - if (isSelected) { - return this.measureSettingElementHeight(tree, element); - } else { - return SettingsRenderer.SETTING_ROW_HEIGHT; + const h = this._getHeight(tree, element); + this.rowHeightCache.set(element.id, h); + return h; + } + + _getHeight(tree: ITree, element: SettingsTreeElement): number { + if (element instanceof SettingsTreeGroupElement) { + if (element.isFirstGroup) { + return 31; } + + return 40 + (7 * element.level); + } + + if (element instanceof SettingsTreeSettingElement) { + if (isExcludeSetting(element.setting)) { + return this._getExcludeSettingHeight(element); + } else { + return this.measureSettingElementHeight(tree, element); + } + } + + if (element instanceof SettingsTreeNewExtensionsElement) { + return 40; } return 0; } - private measureSettingElementHeight(tree: ITree, element: ISettingElement): number { - const measureHelper = DOM.append(this.measureContainer, $('.setting-measure-helper')); - - const template = this.renderSettingTemplate(tree, measureHelper); - this.renderSettingElement(tree, element, template, true); - - const height = measureHelper.offsetHeight; - this.measureContainer.removeChild(measureHelper); - return height; + _getExcludeSettingHeight(element: SettingsTreeSettingElement): number { + const displayValue = getExcludeDisplayValue(element); + return (displayValue.length + 1) * 22 + 66 + this.measureSettingDescription(element); } - getTemplateId(tree: ITree, element: TreeElement): string { - if (element.type === TreeItemType.groupTitle) { + private measureSettingElementHeight(tree: ITree, element: SettingsTreeSettingElement): number { + let heightExcludingDescription = 86; + + if (element.valueType === 'boolean') { + heightExcludingDescription = 60; + } + + return heightExcludingDescription + this.measureSettingDescription(element); + } + + private measureSettingDescription(element: SettingsTreeSettingElement): number { + if (element.description.length < this.longestSingleLineDescription * .8) { + // Most setting descriptions are one short line, so try to avoid measuring them. + // If the description is less than 80% of the longest single line description, assume this will also render to be one line. + return 18; + } + + const boolMeasureClass = 'measure-bool-description'; + if (element.valueType === 'boolean') { + this.descriptionMeasureContainer.classList.add(boolMeasureClass); + } else if (this.descriptionMeasureContainer.classList.contains(boolMeasureClass)) { + this.descriptionMeasureContainer.classList.remove(boolMeasureClass); + } + + const shouldRenderMarkdown = element.setting.descriptionIsMarkdown && element.description.indexOf('\n- ') >= 0; + + while (this.descriptionMeasureContainer.firstChild) { + this.descriptionMeasureContainer.removeChild(this.descriptionMeasureContainer.firstChild); + } + + if (shouldRenderMarkdown) { + const text = fixSettingLinks(element.description); + const rendered = renderMarkdown({ value: text }); + rendered.classList.add('setting-item-description-markdown'); + this.descriptionMeasureContainer.appendChild(rendered); + + return this.descriptionMeasureContainer.offsetHeight; + } else { + // Remove markdown links, setting links, backticks + const measureText = element.setting.descriptionIsMarkdown ? + fixSettingLinks(element.description) + .replace(/\[(.*)\]\(.*\)/g, '$1') + .replace(/`([^`]*)`/g, '$1') : + element.description; + + this.descriptionMeasureContainer.innerText = measureText; + const h = this.descriptionMeasureContainer.offsetHeight; + if (h < 20 && measureText.length > this.longestSingleLineDescription) { + this.longestSingleLineDescription = measureText.length; + } + + return h; + } + } + + getTemplateId(tree: ITree, element: SettingsTreeElement): string { + if (element instanceof SettingsTreeGroupElement) { return SETTINGS_GROUP_ELEMENT_TEMPLATE_ID; } - if (element.type === TreeItemType.setting) { - return SETTINGS_ELEMENT_TEMPLATE_ID; + if (element instanceof SettingsTreeSettingElement) { + if (element.valueType === 'boolean') { + return SETTINGS_BOOL_TEMPLATE_ID; + } + + if (element.valueType === 'integer' || element.valueType === 'number' || element.valueType === 'nullable-integer' || element.valueType === 'nullable-number') { + return SETTINGS_NUMBER_TEMPLATE_ID; + } + + if (element.valueType === 'string') { + return SETTINGS_TEXT_TEMPLATE_ID; + } + + if (element.valueType === 'enum') { + return SETTINGS_ENUM_TEMPLATE_ID; + } + + if (element.valueType === 'exclude') { + return SETTINGS_EXCLUDE_TEMPLATE_ID; + } + + return SETTINGS_COMPLEX_TEMPLATE_ID; + } + + if (element instanceof SettingsTreeNewExtensionsElement) { + return SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID; } return ''; @@ -321,8 +550,32 @@ export class SettingsRenderer implements IRenderer { return this.renderGroupTitleTemplate(container); } - if (templateId === SETTINGS_ELEMENT_TEMPLATE_ID) { - return this.renderSettingTemplate(tree, container); + if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { + return this.renderSettingTextTemplate(tree, container); + } + + if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { + return this.renderSettingNumberTemplate(tree, container); + } + + if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { + return this.renderSettingBoolTemplate(tree, container); + } + + if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { + return this.renderSettingEnumTemplate(tree, container); + } + + if (templateId === SETTINGS_EXCLUDE_TEMPLATE_ID) { + return this.renderSettingExcludeTemplate(tree, container); + } + + if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { + return this.renderSettingComplexTemplate(tree, container); + } + + if (templateId === SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID) { + return this.renderNewExtensionsTemplate(container); } return null; @@ -331,51 +584,52 @@ export class SettingsRenderer implements IRenderer { private renderGroupTitleTemplate(container: HTMLElement): IGroupTitleTemplate { DOM.addClass(container, 'group-title'); - const labelElement = DOM.append(container, $('h3.settings-group-title-label')); - const toDispose = []; const template: IGroupTitleTemplate = { parent: container, - labelElement, toDispose }; return template; } - private renderSettingTemplate(tree: ITree, container: HTMLElement): ISettingItemTemplate { + private renderCommonTemplate(tree: ITree, container: HTMLElement, typeClass: string): ISettingItemTemplate { DOM.addClass(container, 'setting-item'); - - const leftElement = DOM.append(container, $('.setting-item-left')); - const rightElement = DOM.append(container, $('.setting-item-right')); - - const titleElement = DOM.append(leftElement, $('.setting-item-title')); - const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); - const labelElement = DOM.append(titleElement, $('span.setting-item-label')); - const isConfiguredElement = DOM.append(titleElement, $('span.setting-item-is-configured-label')); + DOM.addClass(container, 'setting-item-' + typeClass); + const titleElement = DOM.append(container, $('.setting-item-title')); + const labelCategoryContainer = DOM.append(titleElement, $('.setting-item-cat-label-container')); + const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category')); + const labelElement = DOM.append(labelCategoryContainer, $('span.setting-item-label')); const otherOverridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); - const descriptionElement = DOM.append(leftElement, $('.setting-item-description')); - const expandIndicatorElement = DOM.append(leftElement, $('.expand-indicator')); + const descriptionElement = DOM.append(container, $('.setting-item-description')); + const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); + modifiedIndicatorElement.title = localize('modified', "Modified"); - const valueElement = DOM.append(rightElement, $('.setting-item-value')); + const valueElement = DOM.append(container, $('.setting-item-value')); + const controlElement = DOM.append(valueElement, $('div.setting-item-control')); + + const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = []; + + const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); + const toolbar = this.renderSettingToolbar(toolbarContainer); + const template: ISettingItemTemplate = { - parent: container, toDispose, containerElement: container, categoryElement, labelElement, descriptionElement, - expandIndicatorElement, - valueElement, - isConfiguredElement, - otherOverridesElement + controlElement, + deprecationWarningElement, + otherOverridesElement, + toolbar }; // Prevent clicks from being handled by list - toDispose.push(DOM.addDisposableListener(valueElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); + toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); toDispose.push(DOM.addStandardDisposableListener(valueElement, 'keydown', (e: StandardKeyboardEvent) => { if (e.keyCode === KeyCode.Escape) { @@ -387,75 +641,414 @@ export class SettingsRenderer implements IRenderer { return template; } - renderElement(tree: ITree, element: TreeElement, templateId: string, template: any): void { - if (templateId === SETTINGS_ELEMENT_TEMPLATE_ID) { - return this.renderSettingElement(tree, element, template); + private addSettingElementFocusHandler(template: ISettingItemTemplate): void { + const focusTracker = DOM.trackFocus(template.containerElement); + template.toDispose.push(focusTracker); + focusTracker.onDidBlur(() => { + if (template.containerElement.classList.contains('focused')) { + template.containerElement.classList.remove('focused'); + } + }); + + focusTracker.onDidFocus(() => { + template.containerElement.classList.add('focused'); + + if (template.context) { + this._onDidFocusSetting.fire(template.context); + } + }); + } + + private renderSettingTextTemplate(tree: ITree, container: HTMLElement, type = 'text'): ISettingTextItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'text'); + const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); + + const inputBox = new InputBox(common.controlElement, this.contextViewService); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + inputBox.inputElement.classList.add(SettingsRenderer.CONTROL_CLASS); + + const template: ISettingTextItemTemplate = { + ...common, + inputBox, + validationErrorMessageElement + }; + + this.addSettingElementFocusHandler(template); + + return template; + } + + private renderSettingNumberTemplate(tree: ITree, container: HTMLElement): ISettingNumberItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'number'); + const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); + + const inputBox = new InputBox(common.controlElement, this.contextViewService, { type: 'number' }); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { + inputBackground: settingsNumberInputBackground, + inputForeground: settingsNumberInputForeground, + inputBorder: settingsNumberInputBorder + })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + inputBox.inputElement.classList.add(SettingsRenderer.CONTROL_CLASS); + + const template: ISettingNumberItemTemplate = { + ...common, + inputBox, + validationErrorMessageElement + }; + + this.addSettingElementFocusHandler(template); + + return template; + } + + private renderSettingToolbar(container: HTMLElement): ToolBar { + const toggleMenuKeybinding = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); + let toggleMenuTitle = localize('settingsContextMenuTitle', "More Actions... "); + if (toggleMenuKeybinding) { + toggleMenuTitle += ` (${toggleMenuKeybinding && toggleMenuKeybinding.getLabel()})`; } + const toolbar = new ToolBar(container, this.contextMenuService, { + toggleMenuTitle + }); + toolbar.setActions([], this.settingActions)(); + const button = container.querySelector('.toolbar-toggle-more'); + if (button) { + (button).tabIndex = -1; + } + + return toolbar; + } + + private renderSettingBoolTemplate(tree: ITree, container: HTMLElement): ISettingBoolItemTemplate { + DOM.addClass(container, 'setting-item'); + DOM.addClass(container, 'setting-item-bool'); + + const titleElement = DOM.append(container, $('.setting-item-title')); + const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); + const labelElement = DOM.append(titleElement, $('span.setting-item-label')); + const otherOverridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); + + const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); + const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); + const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); + const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); + modifiedIndicatorElement.title = localize('modified', "Modified"); + + + const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); + + const toDispose = []; + const checkbox = new Checkbox({ actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: null }); + controlElement.appendChild(checkbox.domNode); + toDispose.push(checkbox); + toDispose.push(checkbox.onChange(() => { + if (template.onChange) { + template.onChange(checkbox.checked); + } + })); + + // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety + // Also have to ignore embedded links - too buried to stop propagation + toDispose.push(DOM.addDisposableListener(descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { + const targetElement = e.toElement; + const targetId = descriptionElement.getAttribute('checkbox_label_target_id'); + + // Make sure we are not a link and the target ID matches + // Toggle target checkbox + if (targetElement.tagName.toLowerCase() !== 'a' && targetId === template.checkbox.domNode.id) { + template.checkbox.checked = template.checkbox.checked ? false : true; + template.onChange(checkbox.checked); + } + DOM.EventHelper.stop(e); + })); + + checkbox.domNode.classList.add(SettingsRenderer.CONTROL_CLASS); + const toolbar = this.renderSettingToolbar(container); + toDispose.push(toolbar); + + const template: ISettingBoolItemTemplate = { + toDispose, + + containerElement: container, + categoryElement, + labelElement, + controlElement, + checkbox, + descriptionElement, + deprecationWarningElement, + otherOverridesElement, + toolbar + }; + + this.addSettingElementFocusHandler(template); + + // Prevent clicks from being handled by list + toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); + + toDispose.push(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { + if (e.keyCode === KeyCode.Escape) { + tree.domFocus(); + e.browserEvent.stopPropagation(); + } + })); + + return template; + } + + public cancelSuggesters() { + this.contextViewService.hideContextView(); + } + + private renderSettingEnumTemplate(tree: ITree, container: HTMLElement): ISettingEnumItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'enum'); + + const selectBox = new SelectBox([], undefined, this.contextViewService, undefined, { + hasDetails: true + }); + + common.toDispose.push(selectBox); + common.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService, { + selectBackground: settingsSelectBackground, + selectForeground: settingsSelectForeground, + selectBorder: settingsSelectBorder, + selectListBorder: settingsSelectListBorder + })); + selectBox.render(common.controlElement); + const selectElement = common.controlElement.querySelector('select'); + if (selectElement) { + selectElement.classList.add(SettingsRenderer.CONTROL_CLASS); + } + + common.toDispose.push( + selectBox.onDidSelect(e => { + if (template.onChange) { + template.onChange(e.index); + } + })); + + const enumDescriptionElement = common.containerElement.insertBefore($('.setting-item-enumDescription'), common.descriptionElement.nextSibling); + + const template: ISettingEnumItemTemplate = { + ...common, + selectBox, + enumDescriptionElement + }; + + this.addSettingElementFocusHandler(template); + + return template; + } + + private renderSettingExcludeTemplate(tree: ITree, container: HTMLElement): ISettingExcludeItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'exclude'); + + const excludeWidget = this.instantiationService.createInstance(ExcludeSettingWidget, common.controlElement); + excludeWidget.domNode.classList.add(SettingsRenderer.CONTROL_CLASS); + common.toDispose.push(excludeWidget); + + const template: ISettingExcludeItemTemplate = { + ...common, + excludeWidget + }; + + this.addSettingElementFocusHandler(template); + + common.toDispose.push(excludeWidget.onDidChangeExclude(e => { + if (template.context) { + let newValue = { ...template.context.scopeValue }; + + // first delete the existing entry, if present + if (e.originalPattern) { + if (e.originalPattern in template.context.defaultValue) { + // delete a default by overriding it + newValue[e.originalPattern] = false; + } else { + delete newValue[e.originalPattern]; + } + } + + // then add the new or updated entry, if present + if (e.pattern) { + if (e.pattern in template.context.defaultValue && !e.sibling) { + // add a default by deleting its override + delete newValue[e.pattern]; + } else { + newValue[e.pattern] = e.sibling ? { when: e.sibling } : true; + } + } + + const sortKeys = (obj) => { + const keyArray = Object.keys(obj) + .map(key => ({ key, val: obj[key] })) + .sort((a, b) => a.key.localeCompare(b.key)); + + const retVal = {}; + keyArray.forEach(pair => { + retVal[pair.key] = pair.val; + }); + return retVal; + }; + + this._onDidChangeSetting.fire({ + key: template.context.setting.key, + value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue) + }); + } + })); + + return template; + } + + private renderSettingComplexTemplate(tree: ITree, container: HTMLElement): ISettingComplexItemTemplate { + const common = this.renderCommonTemplate(tree, container, 'complex'); + + const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: null, buttonHoverBackground: null }); + common.toDispose.push(openSettingsButton); + common.toDispose.push(openSettingsButton.onDidClick(() => template.onChange(null))); + openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); + openSettingsButton.element.classList.add('edit-in-settings-button'); + + common.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { + buttonBackground: Color.transparent.toString(), + buttonHoverBackground: Color.transparent.toString(), + buttonForeground: 'foreground' + })); + + const template: ISettingComplexItemTemplate = { + ...common, + button: openSettingsButton + }; + + this.addSettingElementFocusHandler(template); + + return template; + } + + private renderNewExtensionsTemplate(container: HTMLElement): ISettingNewExtensionsTemplate { + const toDispose = []; + + container.classList.add('setting-item-new-extensions'); + + const button = new Button(container, { title: true, buttonBackground: null, buttonHoverBackground: null }); + toDispose.push(button); + toDispose.push(button.onDidClick(() => { + if (template.context) { + this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds); + } + })); + button.label = localize('newExtensionsButtonLabel', "Show matching extensions"); + button.element.classList.add('settings-new-extensions-button'); + toDispose.push(attachButtonStyler(button, this.themeService)); + + const template: ISettingNewExtensionsTemplate = { + button, + toDispose + }; + + // this.addSettingElementFocusHandler(template); + + return template; + } + + renderElement(tree: ITree, element: SettingsTreeElement, templateId: string, template: any): void { if (templateId === SETTINGS_GROUP_ELEMENT_TEMPLATE_ID) { - (template).labelElement.textContent = (element).group.title; - return; + return this.renderGroupElement(element, template); + } + + if (templateId === SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID) { + return this.renderNewExtensionsElement(element, template); + } + + return this.renderSettingElement(tree, element, templateId, template); + } + + private renderGroupElement(element: SettingsTreeGroupElement, template: IGroupTitleTemplate): void { + template.parent.innerHTML = ''; + const labelElement = DOM.append(template.parent, $('div.settings-group-title-label')); + labelElement.classList.add(`settings-group-level-${element.level}`); + labelElement.textContent = (element).label; + + if (element.isFirstGroup) { + labelElement.classList.add('settings-group-first'); } } - private elementIsSelected(tree: ITree, element: TreeElement): boolean { - const selection = tree.getSelection(); - const selectedElement: TreeElement = selection && selection[0]; - return selectedElement && selectedElement.id === element.id; + private renderNewExtensionsElement(element: SettingsTreeNewExtensionsElement, template: ISettingNewExtensionsTemplate): void { + template.context = element; } - private renderSettingElement(tree: ITree, element: ISettingElement, template: ISettingItemTemplate, measuring?: boolean): void { - const isSelected = !!this.elementIsSelected(tree, element); + public getSettingDOMElementForDOMElement(domElement: HTMLElement): HTMLElement { + const parent = DOM.findParentWithClass(domElement, 'setting-item'); + if (parent) { + return parent; + } + + return null; + } + + public getDOMElementsForSettingKey(treeContainer: HTMLElement, key: string): NodeListOf { + return treeContainer.querySelectorAll(`[${SettingsRenderer.SETTING_KEY_ATTR}="${key}"]`); + } + + public getKeyForDOMElementInSetting(element: HTMLElement): string { + const settingElement = this.getSettingDOMElementForDOMElement(element); + return settingElement && settingElement.getAttribute(SettingsRenderer.SETTING_KEY_ATTR); + } + + private renderSettingElement(tree: ITree, element: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { + template.context = element; + template.toolbar.context = element; + const setting = element.setting; - template.context = element; - DOM.toggleClass(template.parent, 'is-configured', element.isConfigured); - DOM.toggleClass(template.parent, 'is-expanded', isSelected); - template.containerElement.id = element.id; + DOM.toggleClass(template.containerElement, 'is-configured', element.isConfigured); + DOM.toggleClass(template.containerElement, 'is-expanded', true); + template.containerElement.setAttribute(SettingsRenderer.SETTING_KEY_ATTR, element.setting.key); - const titleTooltip = setting.key; - template.categoryElement.textContent = element.displayCategory + ': '; + const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); + template.categoryElement.textContent = element.displayCategory && (element.displayCategory + ': '); template.categoryElement.title = titleTooltip; template.labelElement.textContent = element.displayLabel; template.labelElement.title = titleTooltip; - template.descriptionElement.textContent = element.description; - if (!measuring) { - const expandedHeight = this.measureSettingElementHeight(tree, element); - const isExpandable = expandedHeight > SettingsRenderer.SETTING_ROW_HEIGHT; - DOM.toggleClass(template.parent, 'is-expandable', isExpandable); - - if (isSelected) { - template.expandIndicatorElement.innerHTML = renderOcticons('$(chevron-up)'); - } else if (isExpandable) { - template.expandIndicatorElement.innerHTML = renderOcticons('$(chevron-down)'); - } else { - template.expandIndicatorElement.innerHTML = ''; - } + this.renderValue(element, templateId, template); + template.descriptionElement.innerHTML = ''; + if (element.setting.descriptionIsMarkdown) { + const renderedDescription = this.renderDescriptionMarkdown(element, element.description, template.toDispose); + template.descriptionElement.appendChild(renderedDescription); + } else { + template.descriptionElement.innerText = element.description; } - this.renderValue(element, isSelected, template); + const baseId = (element.displayCategory + '_' + element.displayLabel).replace(/ /g, '_').toLowerCase(); + template.descriptionElement.id = baseId + '_setting_description'; - const resetButton = new Button(template.valueElement); - const resetText = localize('resetButtonTitle', "reset"); - resetButton.label = resetText; - resetButton.element.title = resetText; - resetButton.element.classList.add('setting-reset-button'); - resetButton.element.tabIndex = isSelected ? 0 : -1; - - attachButtonStyler(resetButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: editorActiveLinkForeground - }); - - template.toDispose.push(resetButton.onDidClick(e => { - this._onDidChangeSetting.fire({ key: element.setting.key, value: undefined }); - })); - template.toDispose.push(resetButton); - - template.isConfiguredElement.textContent = element.isConfigured ? localize('configured', "Modified") : ''; + if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { + // Add checkbox target to description clickable and able to toggle checkbox + template.descriptionElement.setAttribute('checkbox_label_target_id', baseId + '_setting_item'); + } if (element.overriddenScopeList.length) { let otherOverridesLabel = element.isConfigured ? @@ -463,73 +1056,186 @@ export class SettingsRenderer implements IRenderer { localize('configuredIn', "Modified in"); template.otherOverridesElement.textContent = `(${otherOverridesLabel}: ${element.overriddenScopeList.join(', ')})`; - } - } - - private renderValue(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate): void { - const onChange = value => this._onDidChangeSetting.fire({ key: element.setting.key, value }); - template.valueElement.innerHTML = ''; - if (element.valueType === 'string' && element.enum) { - this.renderEnum(element, isSelected, template, onChange); - } else if (element.valueType === 'boolean') { - this.renderBool(element, isSelected, template, onChange); - } else if (element.valueType === 'string') { - this.renderText(element, isSelected, template, onChange); - } else if (element.valueType === 'number' || element.valueType === 'integer') { - this.renderText(element, isSelected, template, value => onChange(parseInt(value))); } else { - this.renderEditInSettingsJson(element, isSelected, template); + template.otherOverridesElement.textContent = ''; + } + + // Remove tree attributes - sometimes overridden by tree - should be managed there + template.containerElement.parentElement.removeAttribute('role'); + template.containerElement.parentElement.removeAttribute('aria-level'); + template.containerElement.parentElement.removeAttribute('aria-posinset'); + template.containerElement.parentElement.removeAttribute('aria-setsize'); + } + + private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: IDisposable[]): HTMLElement { + // Rewrite `#editor.fontSize#` to link format + text = fixSettingLinks(text); + + const renderedMarkdown = renderMarkdown({ value: text }, { + actionHandler: { + callback: (content: string) => { + if (startsWith(content, '#')) { + const e: ISettingLinkClickEvent = { + source: element, + targetKey: content.substr(1) + }; + this._onDidClickSettingLink.fire(e); + } else { + this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); + } + }, + disposeables + } + }); + + renderedMarkdown.classList.add('setting-item-description-markdown'); + cleanRenderedMarkdown(renderedMarkdown); + return renderedMarkdown; + } + + private renderValue(element: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { + const onChange = value => this._onDidChangeSetting.fire({ key: element.setting.key, value }); + template.deprecationWarningElement.innerText = element.setting.deprecationMessage || ''; + + if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { + this.renderEnum(element, template, onChange); + } else if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { + this.renderText(element, template, onChange); + } else if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { + this.renderNumber(element, template, onChange); + } else if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { + this.renderBool(element, template, onChange); + } else if (templateId === SETTINGS_EXCLUDE_TEMPLATE_ID) { + this.renderExcludeSetting(element, template); + } else if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { + this.renderComplexSetting(element, template); } } - private renderBool(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate, onChange: (value: boolean) => void): void { - const checkboxElement = DOM.append(template.valueElement, $('input.setting-value-checkbox.setting-value-input')); - checkboxElement.type = 'checkbox'; - checkboxElement.checked = element.value; - checkboxElement.tabIndex = isSelected ? 0 : -1; + private renderBool(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { + template.onChange = null; + template.checkbox.checked = dataElement.value; + template.onChange = onChange; + + // Setup and add ARIA attributes + // Create id and label for control/input element - parent is wrapper div + const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); + const modifiedText = dataElement.isConfigured ? 'Modified' : ''; + const label = dataElement.displayCategory + ' ' + dataElement.displayLabel + ' ' + modifiedText; + + // We use the parent control div for the aria-labelledby target + // Does not appear you can use the direct label on the element itself within a tree + template.checkbox.domNode.parentElement.id = baseId + '_setting_label'; + template.checkbox.domNode.parentElement.setAttribute('aria-label', label); + + // Labels will not be read on descendent input elements of the parent treeitem + // unless defined as role=treeitem and indirect aria-labelledby approach + template.checkbox.domNode.id = baseId + '_setting_item'; + template.checkbox.domNode.setAttribute('role', 'checkbox'); + template.checkbox.domNode.setAttribute('aria-labelledby', baseId + '_setting_label'); + template.checkbox.domNode.setAttribute('aria-describedby', baseId + '_setting_description'); - template.toDispose.push(DOM.addDisposableListener(checkboxElement, 'change', e => onChange(checkboxElement.checked))); } - private renderEnum(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate, onChange: (value: string) => void): void { - const idx = element.enum.indexOf(element.value); - const displayOptions = element.enum.map(escapeInvisibleChars); - const selectBox = new SelectBox(displayOptions, idx, this.contextViewService); - template.toDispose.push(selectBox); - template.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService)); - selectBox.render(template.valueElement); - if (template.valueElement.firstElementChild) { - template.valueElement.firstElementChild.setAttribute('tabindex', isSelected ? '0' : '-1'); + private renderEnum(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void { + const displayOptions = dataElement.setting.enum + .map(String) + .map(escapeInvisibleChars); + + template.selectBox.setOptions(displayOptions); + const enumDescriptions = dataElement.setting.enumDescriptions; + const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; + template.selectBox.setDetailsProvider(index => + ({ + details: enumDescriptions && enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index]), + isMarkdown: enumDescriptionsAreMarkdown + })); + + const modifiedText = dataElement.isConfigured ? 'Modified' : ''; + const label = dataElement.displayCategory + ' ' + dataElement.displayLabel + ' ' + modifiedText; + const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); + + template.selectBox.setAriaLabel(label); + + const idx = dataElement.setting.enum.indexOf(dataElement.value); + template.onChange = null; + template.selectBox.select(idx); + template.onChange = idx => onChange(dataElement.setting.enum[idx]); + + if (template.controlElement.firstElementChild) { + // SelectBox needs to have treeitem changed to combobox to read correctly within tree + template.controlElement.firstElementChild.setAttribute('role', 'combobox'); + template.controlElement.firstElementChild.setAttribute('aria-describedby', baseId + '_setting_description'); } - template.toDispose.push( - selectBox.onDidSelect(e => onChange(element.enum[e.index]))); + template.enumDescriptionElement.innerHTML = ''; } - private renderText(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate, onChange: (value: string) => void): void { - const inputBox = new InputBox(template.valueElement, this.contextViewService); - template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService)); - template.toDispose.push(inputBox); - inputBox.value = element.value; - inputBox.inputElement.tabIndex = isSelected ? 0 : -1; + private renderText(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void { + const modifiedText = dataElement.isConfigured ? 'Modified' : ''; + const label = dataElement.displayCategory + ' ' + dataElement.displayLabel + ' ' + modifiedText; template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(value); }; - template.toDispose.push( - inputBox.onDidChange(e => onChange(e))); + // Setup and add ARIA attributes + // Create id and label for control/input element - parent is wrapper div + const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); + + // We use the parent control div for the aria-labelledby target + // Does not appear you can use the direct label on the element itself within a tree + template.inputBox.inputElement.parentElement.id = baseId + '_setting_label'; + template.inputBox.inputElement.parentElement.setAttribute('aria-label', label); + + // Labels will not be read on descendent input elements of the parent treeitem + // unless defined as role=treeitem and indirect aria-labelledby approach + template.inputBox.inputElement.id = baseId + '_setting_item'; + template.inputBox.inputElement.setAttribute('role', 'textbox'); + template.inputBox.inputElement.setAttribute('aria-labelledby', baseId + '_setting_label'); + template.inputBox.inputElement.setAttribute('aria-describedby', baseId + '_setting_description'); + + renderValidations(dataElement, template, true, label); } - private renderEditInSettingsJson(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate): void { - const openSettingsButton = new Button(template.valueElement, { title: true, buttonBackground: null, buttonHoverBackground: null }); - openSettingsButton.onDidClick(() => this._onDidOpenSettings.fire()); - openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); - openSettingsButton.element.classList.add('edit-in-settings-button'); - openSettingsButton.element.tabIndex = isSelected ? 0 : -1; - template.toDispose.push(openSettingsButton); - template.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: 'foreground' - })); + private renderNumber(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: number) => void): void { + const modifiedText = dataElement.isConfigured ? 'Modified' : ''; + const label = dataElement.displayCategory + ' ' + dataElement.displayLabel + ' number ' + modifiedText; const numParseFn = (dataElement.valueType === 'integer' || dataElement.valueType === 'nullable-integer') + ? parseInt : parseFloat; + + const nullNumParseFn = (dataElement.valueType === 'nullable-integer' || dataElement.valueType === 'nullable-number') + ? (v => v === '' ? null : numParseFn(v)) : numParseFn; + + template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(nullNumParseFn(value)); }; + + // Setup and add ARIA attributes + // Create id and label for control/input element - parent is wrapper div + const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); + + // We use the parent control div for the aria-labelledby target + // Does not appear you can use the direct label on the element itself within a tree + template.inputBox.inputElement.parentElement.id = baseId + '_setting_label'; + template.inputBox.inputElement.parentElement.setAttribute('aria-label', label); + + // Labels will not be read on descendent input elements of the parent treeitem + // unless defined as role=treeitem and indirect aria-labelledby approach + template.inputBox.inputElement.id = baseId + '_setting_item'; + template.inputBox.inputElement.setAttribute('role', 'textbox'); + template.inputBox.inputElement.setAttribute('aria-labelledby', baseId + '_setting_label'); + template.inputBox.inputElement.setAttribute('aria-describedby', baseId + '_setting_description'); + + renderValidations(dataElement, template, true, label); + } + + private renderExcludeSetting(dataElement: SettingsTreeSettingElement, template: ISettingExcludeItemTemplate): void { + const value = getExcludeDisplayValue(dataElement); + template.excludeWidget.setValue(value); + template.context = dataElement; + } + + private renderComplexSetting(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate): void { + template.onChange = () => this._onDidOpenSettings.fire(dataElement.setting.key); } disposeTemplate(tree: ITree, templateId: string, template: IDisposableTemplate): void { @@ -537,8 +1243,48 @@ export class SettingsRenderer implements IRenderer { } } +function renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, calledOnStartup: boolean, originalAriaLabel: string) { + if (dataElement.setting.validator) { + let errMsg = dataElement.setting.validator(template.inputBox.value); + if (errMsg) { + DOM.addClass(template.containerElement, 'invalid-input'); + template.validationErrorMessageElement.innerText = errMsg; + let validationError = localize('validationError', "Validation Error."); + template.inputBox.inputElement.parentElement.setAttribute('aria-label', [originalAriaLabel, validationError, errMsg].join(' ')); + if (!calledOnStartup) { ariaAlert(validationError + ' ' + errMsg); } + return; + } else { + template.inputBox.inputElement.parentElement.setAttribute('aria-label', originalAriaLabel); + } + } + DOM.removeClass(template.containerElement, 'invalid-input'); +} + +function cleanRenderedMarkdown(element: Node): void { + for (let i = 0; i < element.childNodes.length; i++) { + const child = element.childNodes.item(i); + + const tagName = (child).tagName && (child).tagName.toLowerCase(); + if (tagName === 'img') { + element.removeChild(child); + } else { + cleanRenderedMarkdown(child); + } + } +} + +function fixSettingLinks(text: string, linkify = true): string { + return text.replace(/`#([^#]*)#`/g, (match, settingKey) => { + const targetDisplayFormat = settingKeyToDisplayFormat(settingKey); + const targetName = `${targetDisplayFormat.category}: ${targetDisplayFormat.label}`; + return linkify ? + `[${targetName}](#${settingKey})` : + `"${targetName}"`; + }); +} + function escapeInvisibleChars(enumValue: string): string { - return enumValue + return enumValue && enumValue .replace(/\n/g, '\\n') .replace(/\r/g, '\\r'); } @@ -546,32 +1292,59 @@ function escapeInvisibleChars(enumValue: string): string { export class SettingsTreeFilter implements IFilter { constructor( private viewState: ISettingsEditorViewState, - @IConfigurationService private configurationService: IConfigurationService ) { } - isVisible(tree: ITree, element: TreeElement): boolean { - if (this.viewState.showConfiguredOnly && element.type === TreeItemType.setting) { - return element.isConfigured; + isVisible(tree: ITree, element: SettingsTreeElement): boolean { + // Filter during search + if (this.viewState.filterToCategory && element instanceof SettingsTreeSettingElement) { + if (!this.settingContainedInGroup(element.setting, this.viewState.filterToCategory)) { + return false; + } } - if (element.type === TreeItemType.groupTitle && this.viewState.showConfiguredOnly) { - return this.groupHasConfiguredSetting(element.group); + // Non-user scope selected + if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER) { + if (!element.matchesScope(this.viewState.settingsTarget)) { + return false; + } + } + + // @modified or tag + if (element instanceof SettingsTreeSettingElement && this.viewState.tagFilters) { + if (!element.matchesAllTags(this.viewState.tagFilters)) { + return false; + } + } + + // Group with no visible children + if (element instanceof SettingsTreeGroupElement) { + if (typeof element.count === 'number') { + return element.count > 0; + } + + return element.children.some(child => this.isVisible(tree, child)); + } + + // Filtered "new extensions" button + if (element instanceof SettingsTreeNewExtensionsElement) { + if ((this.viewState.tagFilters && this.viewState.tagFilters.size) || this.viewState.filterToCategory) { + return false; + } } return true; } - private groupHasConfiguredSetting(group: ISettingsGroup): boolean { - for (let section of group.sections) { - for (let setting of section.settings) { - const { isConfigured } = inspectSetting(setting.key, this.viewState.settingsTarget, this.configurationService); - if (isConfigured) { - return true; - } + private settingContainedInGroup(setting: ISetting, group: SettingsTreeGroupElement): boolean { + return group.children.some(child => { + if (child instanceof SettingsTreeGroupElement) { + return this.settingContainedInGroup(setting, child); + } else if (child instanceof SettingsTreeSettingElement) { + return child.setting.key === setting.key; + } else { + return false; } - } - - return false; + }); } } @@ -581,88 +1354,247 @@ export class SettingsTreeController extends WorkbenchTreeController { ) { super({}, configurationService); } + + protected onLeftClick(tree: ITree, element: any, eventish: IMouseEvent, origin?: string): boolean { + const isLink = eventish.target.tagName.toLowerCase() === 'a' || + eventish.target.parentElement.tagName.toLowerCase() === 'a'; // inside + + if (isLink && (DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement()) || DOM.findParentWithClass(eventish.target, 'select-box-description-markdown'))) { + return true; + } + + return false; + } } export class SettingsAccessibilityProvider implements IAccessibilityProvider { - getAriaLabel(tree: ITree, element: TreeElement): string { + getAriaLabel(tree: ITree, element: SettingsTreeElement): string { if (!element) { return ''; } - if (element.type === TreeItemType.setting) { + if (element instanceof SettingsTreeSettingElement) { return localize('settingRowAriaLabel', "{0} {1}, Setting", element.displayCategory, element.displayLabel); } - if (element.type === TreeItemType.groupTitle) { - return localize('groupRowAriaLabel', "{0}, group", element.group.title); + if (element instanceof SettingsTreeGroupElement) { + return localize('groupRowAriaLabel', "{0}, group", element.label); } return ''; } } -export enum SearchResultIdx { - Local = 0, - Remote = 1 +class NonExpandableOrSelectableTree extends Tree { + expand(): TPromise { + return TPromise.wrap(null); + } + + collapse(): TPromise { + return TPromise.wrap(null); + } + + public setFocus(element?: any, eventPayload?: any): void { + return; + } + + public focusNext(count?: number, eventPayload?: any): void { + return; + } + + public focusPrevious(count?: number, eventPayload?: any): void { + return; + } + + public focusParent(eventPayload?: any): void { + return; + } + + public focusFirstChild(eventPayload?: any): void { + return; + } + + public focusFirst(eventPayload?: any, from?: any): void { + return; + } + + public focusNth(index: number, eventPayload?: any): void { + return; + } + + public focusLast(eventPayload?: any, from?: any): void { + return; + } + + public focusNextPage(eventPayload?: any): void { + return; + } + + public focusPreviousPage(eventPayload?: any): void { + return; + } + + public select(element: any, eventPayload?: any): void { + return; + } + + public selectRange(fromElement: any, toElement: any, eventPayload?: any): void { + return; + } + + public selectAll(elements: any[], eventPayload?: any): void { + return; + } + + public setSelection(elements: any[], eventPayload?: any): void { + return; + } + + public toggleSelection(element: any, eventPayload?: any): void { + return; + } } -export class SearchResultModel { - private rawSearchResults: ISearchResult[]; - private cachedUniqueSearchResults: ISearchResult[]; +export class SettingsTree extends NonExpandableOrSelectableTree { + protected disposables: IDisposable[]; - readonly id = 'searchResultModel'; + constructor( + container: HTMLElement, + viewState: ISettingsEditorViewState, + configuration: Partial, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService + ) { + const treeClass = 'settings-editor-tree'; - getUniqueResults(): ISearchResult[] { - if (this.cachedUniqueSearchResults) { - return this.cachedUniqueSearchResults; - } + const controller = instantiationService.createInstance(SettingsTreeController); + const fullConfiguration = { + controller, + accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), + filter: instantiationService.createInstance(SettingsTreeFilter, viewState), + styler: new DefaultTreestyler(DOM.createStyleSheet(container), treeClass), - if (!this.rawSearchResults) { - return []; - } - - const localMatchKeys = new Set(); - const localResult = objects.deepClone(this.rawSearchResults[SearchResultIdx.Local]); - if (localResult) { - localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); - } - - const remoteResult = objects.deepClone(this.rawSearchResults[SearchResultIdx.Remote]); - if (remoteResult) { - remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key)); - } - - this.cachedUniqueSearchResults = [localResult, remoteResult]; - return this.cachedUniqueSearchResults; - } - - getRawResults(): ISearchResult[] { - return this.rawSearchResults; - } - - setResult(type: SearchResultIdx, result: ISearchResult): void { - this.cachedUniqueSearchResults = null; - this.rawSearchResults = this.rawSearchResults || []; - this.rawSearchResults[type] = result; - } - - resultsAsGroup(): ISettingsGroup { - const flatSettings: ISetting[] = []; - this.getUniqueResults() - .filter(r => !!r) - .forEach(r => { - flatSettings.push( - ...r.filterMatches.map(m => m.setting)); - }); - - return { - id: 'settingsSearchResultGroup', - range: null, - sections: [ - { settings: flatSettings } - ], - title: 'searchResults', - titleRange: null + ...configuration }; + + const options = { + ariaLabel: localize('treeAriaLabel', "Settings"), + showLoading: false, + indentPixels: 0, + twistiePixels: 20, // Actually for gear button + }; + + super(container, + fullConfiguration, + options); + + this.disposables = []; + this.disposables.push(controller); + + this.disposables.push(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const activeBorderColor = theme.getColor(focusBorder); + if (activeBorderColor) { + // TODO@rob - why isn't this applied when added to the stylesheet from tocTree.ts? Seems like a chromium glitch. + collector.addRule(`.settings-editor > .settings-body > .settings-toc-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`); + } + + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid + // applying an opacity to the link color. + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, .9)); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { color: ${fgWithOpacity}; }`); + } + + const errorColor = theme.getColor(errorForeground); + if (errorColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-deprecation-message { color: ${errorColor}; }`); + } + + const invalidInputBackground = theme.getColor(inputValidationErrorBackground); + if (invalidInputBackground) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-validation-message { background-color: ${invalidInputBackground}; }`); + } + + const invalidInputForeground = theme.getColor(inputValidationErrorForeground); + if (invalidInputForeground) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-validation-message { color: ${invalidInputForeground}; }`); + } + + const invalidInputBorder = theme.getColor(inputValidationErrorBorder); + if (invalidInputBorder) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-validation-message { border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); + } + + const headerForegroundColor = theme.getColor(settingsHeaderForeground); + if (headerForegroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-label { color: ${headerForegroundColor}; }`); + } + + const focusBorderColor = theme.getColor(focusBorder); + if (focusBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown a:focus { outline-color: ${focusBorderColor} }`); + } + })); + + this.getHTMLElement().classList.add(treeClass); + + this.disposables.push(attachStyler(themeService, { + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: editorBackground, + listFocusOutline: editorBackground, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground + }, colors => { + this.style(colors); + })); } } + +class CopySettingIdAction extends Action { + static readonly ID = 'settings.copySettingId'; + static readonly LABEL = localize('copySettingIdLabel', "Copy Setting ID"); + + constructor( + @IClipboardService private clipboardService: IClipboardService + ) { + super(CopySettingIdAction.ID, CopySettingIdAction.LABEL); + } + + run(context: SettingsTreeSettingElement): TPromise { + if (context) { + this.clipboardService.writeText(context.setting.key); + } + + return TPromise.as(null); + } +} + +class CopySettingAsJSONAction extends Action { + static readonly ID = 'settings.copySettingAsJSON'; + static readonly LABEL = localize('copySettingAsJSONLabel', "Copy Setting as JSON"); + + constructor( + @IClipboardService private clipboardService: IClipboardService + ) { + super(CopySettingAsJSONAction.ID, CopySettingAsJSONAction.LABEL); + } + + run(context: SettingsTreeSettingElement): TPromise { + if (context) { + const jsonResult = `"${context.setting.key}": ${JSON.stringify(context.value, undefined, ' ')}`; + this.clipboardService.writeText(jsonResult); + } + + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts new file mode 100644 index 00000000000..898f92865c8 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/settingsTreeModels.ts @@ -0,0 +1,544 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as arrays from 'vs/base/common/arrays'; +import { isArray } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { ITOCEntry, knownAcronyms } from 'vs/workbench/parts/preferences/browser/settingsLayout'; +import { IExtensionSetting, ISearchResult, ISetting } from 'vs/workbench/services/preferences/common/preferences'; + +export const MODIFIED_SETTING_TAG = 'modified'; +export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; + +export interface ISettingsEditorViewState { + settingsTarget: SettingsTarget; + tagFilters?: Set; + filterToCategory?: SettingsTreeGroupElement; +} + +export abstract class SettingsTreeElement { + id: string; + parent: SettingsTreeGroupElement; + + /** + * Index assigned in display order, used for paging. + */ + index: number; +} + +export type SettingsTreeGroupChild = (SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement); + +export class SettingsTreeGroupElement extends SettingsTreeElement { + count?: number; + label: string; + level: number; + isFirstGroup: boolean; + + private _childSettingKeys: Set; + private _children: SettingsTreeGroupChild[]; + + get children(): SettingsTreeGroupChild[] { + return this._children; + } + + set children(newChildren: SettingsTreeGroupChild[]) { + this._children = newChildren; + + this._childSettingKeys = new Set(); + this._children.forEach(child => { + if (child instanceof SettingsTreeSettingElement) { + this._childSettingKeys.add(child.setting.key); + } + }); + } + + /** + * Returns whether this group contains the given child key (to a depth of 1 only) + */ + containsSetting(key: string): boolean { + return this._childSettingKeys.has(key); + } +} + +export class SettingsTreeNewExtensionsElement extends SettingsTreeElement { + extensionIds: string[]; +} + +export class SettingsTreeSettingElement extends SettingsTreeElement { + setting: ISetting; + + private _displayCategory: string; + private _displayLabel: string; + + /** + * scopeValue || defaultValue, for rendering convenience. + */ + value: any; + + /** + * The value in the current settings scope. + */ + scopeValue: any; + + /** + * The default value + */ + defaultValue?: any; + + /** + * Whether the setting is configured in the selected scope. + */ + isConfigured: boolean; + + tags?: Set; + overriddenScopeList: string[]; + description: string; + valueType: 'enum' | 'string' | 'integer' | 'number' | 'boolean' | 'exclude' | 'complex' | 'nullable-integer' | 'nullable-number'; + + constructor(setting: ISetting, parent: SettingsTreeGroupElement, index: number, inspectResult: IInspectResult) { + super(); + this.index = index; + this.setting = setting; + this.parent = parent; + this.id = sanitizeId(parent.id + '_' + setting.key); + + this.update(inspectResult); + } + + get displayCategory(): string { + if (!this._displayCategory) { + this.initLabel(); + } + + return this._displayCategory; + } + + get displayLabel(): string { + if (!this._displayLabel) { + this.initLabel(); + } + + return this._displayLabel; + } + + private initLabel(): void { + const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent.id); + this._displayLabel = displayKeyFormat.label; + this._displayCategory = displayKeyFormat.category; + } + + update(inspectResult: IInspectResult): void { + const { isConfigured, inspected, targetSelector } = inspectResult; + + const displayValue = isConfigured ? inspected[targetSelector] : inspected.default; + const overriddenScopeList = []; + if (targetSelector === 'user' && typeof inspected.workspace !== 'undefined') { + overriddenScopeList.push(localize('workspace', "Workspace")); + } + + if (targetSelector === 'workspace' && typeof inspected.user !== 'undefined') { + overriddenScopeList.push(localize('user', "User")); + } + + this.value = displayValue; + this.scopeValue = isConfigured && inspected[targetSelector]; + this.defaultValue = inspected.default; + + this.isConfigured = isConfigured; + if (isConfigured || this.setting.tags || this.tags) { + // Don't create an empty Set for all 1000 settings, only if needed + this.tags = new Set(); + if (isConfigured) { + this.tags.add(MODIFIED_SETTING_TAG); + } + + if (this.setting.tags) { + this.setting.tags.forEach(tag => this.tags.add(tag)); + } + } + + this.overriddenScopeList = overriddenScopeList; + this.description = this.setting.description.join('\n'); + + if (this.setting.enum && (!this.setting.type || settingTypeEnumRenderable(this.setting.type))) { + this.valueType = 'enum'; + } else if (this.setting.type === 'string') { + this.valueType = 'string'; + } else if (isExcludeSetting(this.setting)) { + this.valueType = 'exclude'; + } else if (this.setting.type === 'integer') { + this.valueType = 'integer'; + } else if (this.setting.type === 'number') { + this.valueType = 'number'; + } else if (this.setting.type === 'boolean') { + this.valueType = 'boolean'; + } else if (isArray(this.setting.type) && this.setting.type.indexOf('null') > -1 && this.setting.type.length === 2) { + if (this.setting.type.indexOf('integer') > -1) { + this.valueType = 'nullable-integer'; + } else if (this.setting.type.indexOf('number') > -1) { + this.valueType = 'nullable-number'; + } else { + this.valueType = 'complex'; + } + } else { + this.valueType = 'complex'; + } + } + + matchesAllTags(tagFilters?: Set): boolean { + if (!tagFilters || !tagFilters.size) { + return true; + } + + if (this.tags) { + let hasFilteredTag = true; + tagFilters.forEach(tag => { + hasFilteredTag = hasFilteredTag && this.tags.has(tag); + }); + return hasFilteredTag; + } else { + return false; + } + } + + matchesScope(scope: SettingsTarget): boolean { + const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope; + + if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) { + return this.setting.scope === ConfigurationScope.RESOURCE; + } + + if (configTarget === ConfigurationTarget.WORKSPACE) { + return this.setting.scope === ConfigurationScope.WINDOW || this.setting.scope === ConfigurationScope.RESOURCE; + } + + return true; + } +} + +export class SettingsTreeModel { + protected _root: SettingsTreeGroupElement; + protected _treeElementsById = new Map(); + private _treeElementsBySettingName = new Map(); + private _tocRoot: ITOCEntry; + + constructor( + protected _viewState: ISettingsEditorViewState, + @IConfigurationService private _configurationService: IConfigurationService + ) { } + + get root(): SettingsTreeGroupElement { + return this._root; + } + + update(newTocRoot = this._tocRoot): void { + this._treeElementsById.clear(); + this._treeElementsBySettingName.clear(); + + const newRoot = this.createSettingsTreeGroupElement(newTocRoot); + if (newRoot.children[0] instanceof SettingsTreeGroupElement) { + (newRoot.children[0]).isFirstGroup = true; // TODO + } + + if (this._root) { + this._root.children = newRoot.children; + } else { + this._root = newRoot; + } + } + + getElementById(id: string): SettingsTreeElement { + return this._treeElementsById.get(id); + } + + getElementsByName(name: string): SettingsTreeSettingElement[] { + return this._treeElementsBySettingName.get(name); + } + + updateElementsByName(name: string): void { + if (!this._treeElementsBySettingName.has(name)) { + return; + } + + this._treeElementsBySettingName.get(name).forEach(element => { + const inspectResult = inspectSetting(element.setting.key, this._viewState.settingsTarget, this._configurationService); + element.update(inspectResult); + }); + } + + private createSettingsTreeGroupElement(tocEntry: ITOCEntry, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement { + const element = new SettingsTreeGroupElement(); + const index = this._treeElementsById.size; + element.index = index; + element.id = tocEntry.id; + element.label = tocEntry.label; + element.parent = parent; + element.level = this.getDepth(element); + + const children = []; + if (tocEntry.settings) { + const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element)) + .filter(el => el.setting.deprecationMessage ? el.isConfigured : true); + children.push(...settingChildren); + } + + if (tocEntry.children) { + const groupChildren = tocEntry.children.map(child => this.createSettingsTreeGroupElement(child, element)); + children.push(...groupChildren); + } + + element.children = children; + + this._treeElementsById.set(element.id, element); + return element; + } + + private getDepth(element: SettingsTreeElement): number { + if (element.parent) { + return 1 + this.getDepth(element.parent); + } else { + return 0; + } + } + + private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { + const index = this._treeElementsById.size; + const inspectResult = inspectSetting(setting.key, this._viewState.settingsTarget, this._configurationService); + const element = new SettingsTreeSettingElement(setting, parent, index, inspectResult); + this._treeElementsById.set(element.id, element); + + const nameElements = this._treeElementsBySettingName.get(setting.key) || []; + nameElements.push(element); + this._treeElementsBySettingName.set(setting.key, nameElements); + return element; + } +} + +interface IInspectResult { + isConfigured: boolean; + inspected: any; + targetSelector: string; +} + +function inspectSetting(key: string, target: SettingsTarget, configurationService: IConfigurationService): IInspectResult { + const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; + const inspected = configurationService.inspect(key, inspectOverrides); + const targetSelector = target === ConfigurationTarget.USER ? 'user' : + target === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'workspaceFolder'; + const isConfigured = typeof inspected[targetSelector] !== 'undefined'; + + return { isConfigured, inspected, targetSelector }; +} + +function sanitizeId(id: string): string { + return id.replace(/[\.\/]/, '_'); +} + +export function settingKeyToDisplayFormat(key: string, groupId = ''): { category: string, label: string } { + const lastDotIdx = key.lastIndexOf('.'); + let category = ''; + if (lastDotIdx >= 0) { + category = key.substr(0, lastDotIdx); + key = key.substr(lastDotIdx + 1); + } + + groupId = groupId.replace(/\//g, '.'); + category = trimCategoryForGroup(category, groupId); + category = wordifyKey(category); + + const label = wordifyKey(key); + return { category, label }; +} + +function wordifyKey(key: string): string { + return key + .replace(/\.([a-z])/g, (match, p1) => ` › ${p1.toUpperCase()}`) + .replace(/([a-z])([A-Z])/g, '$1 $2') // fooBar => foo Bar + .replace(/^[a-z]/g, match => match.toUpperCase()) // foo => Foo + .replace(/\b\w+\b/g, match => { + return knownAcronyms.has(match.toLowerCase()) ? + match.toUpperCase() : + match; + }); +} + +function trimCategoryForGroup(category: string, groupId: string): string { + const doTrim = forward => { + const parts = groupId.split('.'); + while (parts.length) { + const reg = new RegExp(`^${parts.join('\\.')}(\\.|$)`, 'i'); + if (reg.test(category)) { + return category.replace(reg, ''); + } + + if (forward) { + parts.pop(); + } else { + parts.shift(); + } + } + + return null; + }; + + let trimmed = doTrim(true); + if (trimmed === null) { + trimmed = doTrim(false); + } + + if (trimmed === null) { + trimmed = category; + } + + return trimmed; +} + +export function isExcludeSetting(setting: ISetting): boolean { + return setting.key === 'files.exclude' || + setting.key === 'search.exclude' || + setting.key === 'files.watcherExclude'; +} + +function settingTypeEnumRenderable(_type: string | string[]) { + const enumRenderableSettingTypes = ['string', 'boolean', 'null', 'integer', 'number']; + let type = isArray(_type) ? _type : [_type]; + return type.every(type => enumRenderableSettingTypes.indexOf(type) > -1); +} + +export const enum SearchResultIdx { + Local = 0, + Remote = 1, + NewExtensions = 2 +} + +export class SearchResultModel extends SettingsTreeModel { + private rawSearchResults: ISearchResult[]; + private cachedUniqueSearchResults: ISearchResult[]; + private newExtensionSearchResults: ISearchResult; + + readonly id = 'searchResultModel'; + + constructor( + viewState: ISettingsEditorViewState, + @IConfigurationService configurationService: IConfigurationService + ) { + super(viewState, configurationService); + this.update({ id: 'searchResultModel', label: '' }); + } + + getUniqueResults(): ISearchResult[] { + if (this.cachedUniqueSearchResults) { + return this.cachedUniqueSearchResults; + } + + if (!this.rawSearchResults) { + return []; + } + + const localMatchKeys = new Set(); + const localResult = this.rawSearchResults[SearchResultIdx.Local]; + if (localResult) { + localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); + } + + const remoteResult = this.rawSearchResults[SearchResultIdx.Remote]; + if (remoteResult) { + remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key)); + } + + if (remoteResult) { + this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions]; + } + + this.cachedUniqueSearchResults = [localResult, remoteResult]; + return this.cachedUniqueSearchResults; + } + + getRawResults(): ISearchResult[] { + return this.rawSearchResults; + } + + setResult(order: SearchResultIdx, result: ISearchResult): void { + this.cachedUniqueSearchResults = null; + this.rawSearchResults = this.rawSearchResults || []; + if (!result) { + delete this.rawSearchResults[order]; + return; + } + + this.rawSearchResults[order] = result; + this.updateChildren(); + } + + updateChildren(): void { + this.update({ + id: 'searchResultModel', + label: 'searchResultModel', + settings: this.getFlatSettings() + }); + + // Save time, filter children in the search model instead of relying on the tree filter, which still requires heights to be calculated. + this.root.children = this.root.children + .filter(child => child instanceof SettingsTreeSettingElement && child.matchesAllTags(this._viewState.tagFilters) && child.matchesScope(this._viewState.settingsTarget)); + + if (this.newExtensionSearchResults && this.newExtensionSearchResults.filterMatches.length) { + const newExtElement = new SettingsTreeNewExtensionsElement(); + newExtElement.index = this._treeElementsById.size; + newExtElement.parent = this._root; + newExtElement.id = 'newExtensions'; + this._treeElementsById.set(newExtElement.id, newExtElement); + + const resultExtensionIds = this.newExtensionSearchResults.filterMatches + .map(result => (result.setting)) + .filter(setting => setting.extensionName && setting.extensionPublisher) + .map(setting => `${setting.extensionPublisher}.${setting.extensionName}`); + newExtElement.extensionIds = arrays.distinct(resultExtensionIds); + this._root.children.push(newExtElement); + } + } + + private getFlatSettings(): ISetting[] { + const flatSettings: ISetting[] = []; + this.getUniqueResults() + .filter(r => !!r) + .forEach(r => { + flatSettings.push( + ...r.filterMatches.map(m => m.setting)); + }); + + return flatSettings; + } +} + +export interface IParsedQuery { + tags: string[]; + query: string; +} + +const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g; +export function parseQuery(query: string): IParsedQuery { + const tags: string[] = []; + query = query.replace(tagRegex, (_, __, quotedTag, tag) => { + tags.push(tag || quotedTag); + return ''; + }); + + query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => { + tags.push(MODIFIED_SETTING_TAG); + return ''; + }); + + query = query.trim(); + + return { + tags, + query + }; +} diff --git a/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts b/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts new file mode 100644 index 00000000000..ae0ad8a2d15 --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts @@ -0,0 +1,482 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IAction } from 'vs/base/common/actions'; +import { Color, RGBA } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/settingsWidgets'; +import { localize } from 'vs/nls'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; + +const $ = DOM.$; +export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "(For settings editor preview) The foreground color for a section header or active title.")); +export const modifiedItemIndicator = registerColor('settings.modifiedItemIndicator', { + light: new Color(new RGBA(102, 175, 224)), + dark: new Color(new RGBA(12, 125, 157)), + hc: new Color(new RGBA(0, 73, 122)) +}, localize('modifiedItemForeground', "(For settings editor preview) The color of the modified setting indicator.")); + +// Enum control colors +export const settingsSelectBackground = registerColor('settings.dropdownBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsDropdownBackground', "(For settings editor preview) Settings editor dropdown background.")); +export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsDropdownForeground', "(For settings editor preview) Settings editor dropdown foreground.")); +export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsDropdownBorder', "(For settings editor preview) Settings editor dropdown border.")); +export const settingsSelectListBorder = registerColor('settings.dropdownListBorder', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, localize('settingsDropdownListBorder', "(For settings editor preview) Settings editor dropdown list border. This surrounds the options and separates the options from the description.")); + +// Bool control colors +export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsCheckboxBackground', "(For settings editor preview) Settings editor checkbox background.")); +export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsCheckboxForeground', "(For settings editor preview) Settings editor checkbox foreground.")); +export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsCheckboxBorder', "(For settings editor preview) Settings editor checkbox border.")); + +// Text control colors +export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "(For settings editor preview) Settings editor text input box background.")); +export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "(For settings editor preview) Settings editor text input box foreground.")); +export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "(For settings editor preview) Settings editor text input box border.")); + +// Number control colors +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "(For settings editor preview) Settings editor number input box background.")); +export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "(For settings editor preview) Settings editor number input box foreground.")); +export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "(For settings editor preview) Settings editor number input box border.")); + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); + if (checkboxBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { background-color: ${checkboxBackgroundColor} !important; }`); + } + + const checkboxBorderColor = theme.getColor(settingsCheckboxBorder); + if (checkboxBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-value-checkbox { border-color: ${checkboxBorderColor} !important; }`); + } + + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown a { color: ${link}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown a > code { color: ${link}; }`); + collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a { color: ${link}; }`); + collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a > code { color: ${link}; }`); + } + + const headerForegroundColor = theme.getColor(settingsHeaderForeground); + if (headerForegroundColor) { + collector.addRule(`.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label.checked { color: ${headerForegroundColor}; border-bottom-color: ${headerForegroundColor}; }`); + } + + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + collector.addRule(`.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label { color: ${foregroundColor}; }`); + } + + // Exclude control + const listHoverBackgroundColor = theme.getColor(listHoverBackground); + if (listHoverBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover { background-color: ${listHoverBackgroundColor}; }`); + } + + const listHoverForegroundColor = theme.getColor(listHoverForeground); + if (listHoverForegroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover { color: ${listHoverForegroundColor}; }`); + } + + const listSelectBackgroundColor = theme.getColor(listActiveSelectionBackground); + if (listSelectBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:focus { background-color: ${listSelectBackgroundColor}; }`); + } + + const listInactiveSelectionBackgroundColor = theme.getColor(listInactiveSelectionBackground); + if (listInactiveSelectionBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:not(:focus) { background-color: ${listInactiveSelectionBackgroundColor}; }`); + } + + const listInactiveSelectionForegroundColor = theme.getColor(listInactiveSelectionForeground); + if (listInactiveSelectionForegroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:not(:focus) { color: ${listInactiveSelectionForegroundColor}; }`); + } + + const listSelectForegroundColor = theme.getColor(listActiveSelectionForeground); + if (listSelectForegroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:focus { color: ${listSelectForegroundColor}; }`); + } + + const codeTextForegroundColor = theme.getColor(textPreformatForeground); + if (codeTextForegroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown code { color: ${codeTextForegroundColor} }`); + collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code { color: ${codeTextForegroundColor} }`); + + } + + const modifiedItemIndicatorColor = theme.getColor(modifiedItemIndicator); + if (modifiedItemIndicatorColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item > .setting-item-modified-indicator { border-color: ${modifiedItemIndicatorColor}; }`); + } +}); + +export class ExcludeSettingListModel { + private _dataItems: IExcludeDataItem[] = []; + private _editKey: string | null; + private _selectedIdx: number | null; + + get items(): IExcludeViewItem[] { + const items = this._dataItems.map((item, i) => { + const editing = item.pattern === this._editKey; + return { + ...item, + editing, + selected: i === this._selectedIdx || editing + }; + }); + + if (this._editKey === '') { + items.push({ + editing: true, + selected: true, + pattern: '', + sibling: '' + }); + } + + return items; + } + + setEditKey(key: string): void { + this._editKey = key; + } + + setValue(excludeData: IExcludeDataItem[]): void { + this._dataItems = excludeData; + } + + select(idx: number): void { + this._selectedIdx = idx; + } + + getSelected(): number { + return this._selectedIdx; + } + + selectNext(): void { + if (typeof this._selectedIdx === 'number') { + this._selectedIdx = Math.min(this._selectedIdx + 1, this._dataItems.length - 1); + } else { + this._selectedIdx = 0; + } + } + + selectPrevious(): void { + if (typeof this._selectedIdx === 'number') { + this._selectedIdx = Math.max(this._selectedIdx - 1, 0); + } else { + this._selectedIdx = 0; + } + } +} + +interface IExcludeChangeEvent { + originalPattern: string; + pattern: string; + sibling?: string; +} + +export class ExcludeSettingWidget extends Disposable { + private listElement: HTMLElement; + private listDisposables: IDisposable[] = []; + + private model = new ExcludeSettingListModel(); + + private readonly _onDidChangeExclude: Emitter = new Emitter(); + public readonly onDidChangeExclude: Event = this._onDidChangeExclude.event; + + get domNode(): HTMLElement { + return this.listElement; + } + + constructor( + private container: HTMLElement, + @IThemeService private themeService: IThemeService, + @IContextViewService private contextViewService: IContextViewService + ) { + super(); + + this.listElement = DOM.append(container, $('.setting-exclude-widget')); + this.listElement.setAttribute('tabindex', '0'); + DOM.append(container, this.renderAddButton()); + this.renderList(); + + this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.CLICK, e => this.onListClick(e))); + this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.DBLCLICK, e => this.onListDoubleClick(e))); + + this._register(DOM.addStandardDisposableListener(this.listElement, 'keydown', (e: KeyboardEvent) => { + if (e.keyCode === KeyCode.UpArrow) { + this.model.selectPrevious(); + this.renderList(); + e.preventDefault(); + e.stopPropagation(); + } else if (e.keyCode === KeyCode.DownArrow) { + this.model.selectNext(); + this.renderList(); + e.preventDefault(); + e.stopPropagation(); + } + })); + } + + setValue(excludeData: IExcludeDataItem[]): void { + this.model.setValue(excludeData); + this.renderList(); + } + + private onListClick(e: MouseEvent): void { + const targetIdx = this.getClickedItemIndex(e); + if (targetIdx < 0) { + return; + } + + if (this.model.getSelected() === targetIdx) { + return; + } + + this.model.select(targetIdx); + this.renderList(); + e.preventDefault(); + e.stopPropagation(); + } + + private onListDoubleClick(e: MouseEvent): void { + const targetIdx = this.getClickedItemIndex(e); + if (targetIdx < 0) { + return; + } + + const item = this.model.items[targetIdx]; + if (item) { + this.editSetting(item.pattern); + e.preventDefault(); + e.stopPropagation(); + } + } + + private getClickedItemIndex(e: MouseEvent): number { + if (!e.target) { + return -1; + } + + const actionbar = DOM.findParentWithClass(e.target, 'monaco-action-bar'); + if (actionbar) { + // Don't handle doubleclicks inside the action bar + return -1; + } + + const element = DOM.findParentWithClass((e.target), 'setting-exclude-row'); + if (!element) { + return -1; + } + + const targetIdxStr = element.getAttribute('data-index'); + if (!targetIdxStr) { + return -1; + } + + const targetIdx = parseInt(targetIdxStr); + return targetIdx; + } + + private renderList(): void { + const focused = DOM.isAncestor(document.activeElement, this.listElement); + + DOM.clearNode(this.listElement); + this.listDisposables = dispose(this.listDisposables); + + const newMode = this.model.items.some(item => item.editing && !item.pattern); + DOM.toggleClass(this.container, 'setting-exclude-new-mode', newMode); + + this.model.items + .map((item, i) => this.renderItem(item, i, focused)) + .forEach(itemElement => this.listElement.appendChild(itemElement)); + + const listHeight = 22 * this.model.items.length; + this.listElement.style.height = listHeight + 'px'; + } + + private createDeleteAction(key: string): IAction { + return { + class: 'setting-excludeAction-remove', + enabled: true, + id: 'workbench.action.removeExcludeItem', + tooltip: localize('removeExcludeItem', "Remove Exclude Item"), + run: () => this._onDidChangeExclude.fire({ originalPattern: key, pattern: undefined }) + }; + } + + private createEditAction(key: string): IAction { + return { + class: 'setting-excludeAction-edit', + enabled: true, + id: 'workbench.action.editExcludeItem', + tooltip: localize('editExcludeItem', "Edit Exclude Item"), + run: () => { + this.editSetting(key); + } + }; + } + + private editSetting(key: string): void { + this.model.setEditKey(key); + this.renderList(); + } + + private renderItem(item: IExcludeViewItem, idx: number, listFocused: boolean): HTMLElement { + return item.editing ? + this.renderEditItem(item) : + this.renderDataItem(item, idx, listFocused); + } + + private renderDataItem(item: IExcludeViewItem, idx: number, listFocused: boolean): HTMLElement { + const rowElement = $('.setting-exclude-row'); + rowElement.setAttribute('data-index', idx + ''); + rowElement.setAttribute('tabindex', item.selected ? '0' : '-1'); + DOM.toggleClass(rowElement, 'selected', item.selected); + + const actionBar = new ActionBar(rowElement); + this.listDisposables.push(actionBar); + + const patternElement = DOM.append(rowElement, $('.setting-exclude-pattern')); + const siblingElement = DOM.append(rowElement, $('.setting-exclude-sibling')); + patternElement.textContent = item.pattern; + siblingElement.textContent = item.sibling && ('when: ' + item.sibling); + + actionBar.push([ + this.createEditAction(item.pattern), + this.createDeleteAction(item.pattern) + ], { icon: true, label: false }); + + rowElement.title = item.sibling ? + localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.pattern, item.sibling) : + localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.pattern); + + if (item.selected) { + if (listFocused) { + setTimeout(() => { + rowElement.focus(); + }, 10); + } + } + + return rowElement; + } + + private renderAddButton(): HTMLElement { + const rowElement = $('.setting-exclude-new-row'); + + const startAddButton = this._register(new Button(rowElement)); + startAddButton.label = localize('addPattern', "Add Pattern"); + startAddButton.element.classList.add('setting-exclude-addButton'); + this._register(attachButtonStyler(startAddButton, this.themeService)); + + this._register(startAddButton.onDidClick(() => { + this.model.setEditKey(''); + this.renderList(); + })); + + return rowElement; + } + + private renderEditItem(item: IExcludeViewItem): HTMLElement { + const rowElement = $('.setting-exclude-edit-row'); + + const onSubmit = edited => { + this.model.setEditKey(null); + const pattern = patternInput.value.trim(); + if (edited && pattern) { + this._onDidChangeExclude.fire({ + originalPattern: item.pattern, + pattern, + sibling: siblingInput && siblingInput.value.trim() + }); + } + this.renderList(); + }; + + const onKeydown = (e: StandardKeyboardEvent) => { + if (e.equals(KeyCode.Enter)) { + onSubmit(true); + } else if (e.equals(KeyCode.Escape)) { + onSubmit(false); + e.preventDefault(); + } + }; + + const patternInput = new InputBox(rowElement, this.contextViewService, { + placeholder: localize('excludePatternInputPlaceholder', "Exclude Pattern...") + }); + patternInput.element.classList.add('setting-exclude-patternInput'); + this.listDisposables.push(attachInputBoxStyler(patternInput, this.themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); + this.listDisposables.push(patternInput); + patternInput.value = item.pattern; + this.listDisposables.push(DOM.addStandardDisposableListener(patternInput.inputElement, DOM.EventType.KEY_DOWN, onKeydown)); + + let siblingInput: InputBox; + if (item.sibling) { + siblingInput = new InputBox(rowElement, this.contextViewService, { + placeholder: localize('excludeSiblingInputPlaceholder', "When Pattern Is Present...") + }); + siblingInput.element.classList.add('setting-exclude-siblingInput'); + this.listDisposables.push(siblingInput); + this.listDisposables.push(attachInputBoxStyler(siblingInput, this.themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); + siblingInput.value = item.sibling; + this.listDisposables.push(DOM.addStandardDisposableListener(siblingInput.inputElement, DOM.EventType.KEY_DOWN, onKeydown)); + } + + const okButton = this._register(new Button(rowElement)); + okButton.label = localize('okButton', "OK"); + okButton.element.classList.add('setting-exclude-okButton'); + this.listDisposables.push(attachButtonStyler(okButton, this.themeService)); + this.listDisposables.push(okButton.onDidClick(() => onSubmit(true))); + + const cancelButton = this._register(new Button(rowElement)); + cancelButton.label = localize('cancelButton', "Cancel"); + cancelButton.element.classList.add('setting-exclude-cancelButton'); + this.listDisposables.push(attachButtonStyler(cancelButton, this.themeService)); + this.listDisposables.push(cancelButton.onDidClick(() => onSubmit(false))); + + setTimeout(() => { + patternInput.focus(); + patternInput.select(); + }, 0); + + return rowElement; + } + + dispose() { + super.dispose(); + this.listDisposables = dispose(this.listDisposables); + } +} + +export interface IExcludeDataItem { + pattern: string; + sibling?: string; +} + +interface IExcludeViewItem extends IExcludeDataItem { + editing?: boolean; + selected?: boolean; +} diff --git a/src/vs/workbench/parts/preferences/browser/tocTree.ts b/src/vs/workbench/parts/preferences/browser/tocTree.ts new file mode 100644 index 00000000000..b015b5af6fd --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/tocTree.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDataSource, IRenderer, ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; +import { DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { SettingsAccessibilityProvider, SettingsTreeFilter } from 'vs/workbench/parts/preferences/browser/settingsTree'; +import { ISettingsEditorViewState, SearchResultModel, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { settingsHeaderForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; + +const $ = DOM.$; + +export class TOCTreeModel { + + private _currentSearchModel: SearchResultModel; + private _settingsTreeRoot: SettingsTreeGroupElement; + + constructor(private _viewState: ISettingsEditorViewState) { + } + + public get settingsTreeRoot(): SettingsTreeGroupElement { + return this._settingsTreeRoot; + } + + public set settingsTreeRoot(value: SettingsTreeGroupElement) { + this._settingsTreeRoot = value; + this.update(); + } + + public set currentSearchModel(model: SearchResultModel) { + this._currentSearchModel = model; + this.update(); + } + + public get children(): SettingsTreeElement[] { + return this._settingsTreeRoot.children; + } + + public update(): void { + this.updateGroupCount(this._settingsTreeRoot); + } + + private updateGroupCount(group: SettingsTreeGroupElement): void { + group.children.forEach(child => { + if (child instanceof SettingsTreeGroupElement) { + this.updateGroupCount(child); + } + }); + + const childCount = group.children + .filter(child => child instanceof SettingsTreeGroupElement) + .reduce((acc, cur) => acc + (cur).count, 0); + + group.count = childCount + this.getGroupCount(group); + } + + private getGroupCount(group: SettingsTreeGroupElement): number { + return group.children.filter(child => { + if (!(child instanceof SettingsTreeSettingElement)) { + return false; + } + + if (this._currentSearchModel && !this._currentSearchModel.root.containsSetting(child.setting.key)) { + return false; + } + + // Check everything that the SettingsFilter checks except whether it's filtered by a category + return child.matchesScope(this._viewState.settingsTarget) && child.matchesAllTags(this._viewState.tagFilters); + }).length; + } +} + +export type TOCTreeElement = SettingsTreeGroupElement | TOCTreeModel; + +export class TOCDataSource implements IDataSource { + constructor(private _treeFilter: SettingsTreeFilter) { + } + + getId(tree: ITree, element: SettingsTreeGroupElement): string { + return element.id; + } + + hasChildren(tree: ITree, element: TOCTreeElement): boolean { + if (element instanceof TOCTreeModel) { + return true; + } + + if (element instanceof SettingsTreeGroupElement) { + // Should have child which won't be filtered out + return element.children && element.children.some(child => child instanceof SettingsTreeGroupElement && this._treeFilter.isVisible(tree, child)); + } + + return false; + } + + getChildren(tree: ITree, element: TOCTreeElement): TPromise { + return TPromise.as(this._getChildren(element)); + } + + private _getChildren(element: TOCTreeElement): SettingsTreeElement[] { + return (element.children) + .filter(child => child instanceof SettingsTreeGroupElement); + } + + getParent(tree: ITree, element: TOCTreeElement): TPromise { + return TPromise.wrap(element instanceof SettingsTreeGroupElement && element.parent); + } +} + +const TOC_ENTRY_TEMPLATE_ID = 'settings.toc.entry'; + +interface ITOCEntryTemplate { + labelElement: HTMLElement; + countElement: HTMLElement; +} + +export class TOCRenderer implements IRenderer { + getHeight(tree: ITree, element: SettingsTreeElement): number { + return 22; + } + + getTemplateId(tree: ITree, element: SettingsTreeElement): string { + return TOC_ENTRY_TEMPLATE_ID; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITOCEntryTemplate { + return { + labelElement: DOM.append(container, $('.settings-toc-entry')), + countElement: DOM.append(container, $('.settings-toc-count')) + }; + } + + renderElement(tree: ITree, element: SettingsTreeGroupElement, templateId: string, template: ITOCEntryTemplate): void { + const count = element.count; + const label = element.label; + + DOM.toggleClass(template.labelElement, 'no-results', count === 0); + template.labelElement.textContent = label; + + if (count) { + template.countElement.textContent = ` (${count})`; + } else { + template.countElement.textContent = ''; + } + } + + disposeTemplate(tree: ITree, templateId: string, templateData: any): void { + } +} + +export class TOCTree extends WorkbenchTree { + constructor( + container: HTMLElement, + viewState: ISettingsEditorViewState, + configuration: Partial, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + const treeClass = 'settings-toc-tree'; + + const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); + const fullConfiguration = { + controller: instantiationService.createInstance(WorkbenchTreeController, {}), + filter, + styler: new DefaultTreestyler(DOM.createStyleSheet(container), treeClass), + dataSource: instantiationService.createInstance(TOCDataSource, filter), + accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), + + ...configuration + }; + + const options: ITreeOptions = { + showLoading: false, + twistiePixels: 15, + horizontalScrollMode: ScrollbarVisibility.Hidden + }; + + super(container, + fullConfiguration, + options, + contextKeyService, + listService, + themeService, + instantiationService, + configurationService); + + this.getHTMLElement().classList.add(treeClass); + + this.disposables.push(attachStyler(themeService, { + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: settingsHeaderForeground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: settingsHeaderForeground, + listFocusBackground: editorBackground, + listFocusForeground: settingsHeaderForeground, + listHoverForeground: settingsHeaderForeground, + listHoverBackground: editorBackground, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: settingsHeaderForeground, + }, colors => { + this.style(colors); + })); + } +} diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index fdf684a9d5a..8d084c1559a 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -10,6 +10,7 @@ import { join } from 'vs/base/common/paths'; import { ISettingsEditorModel, ISearchResult } from 'vs/workbench/services/preferences/common/preferences'; import { IEditor } from 'vs/workbench/common/editor'; import { IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IWorkbenchSettingsConfiguration { workbench: { @@ -40,7 +41,7 @@ export interface IPreferencesSearchService { } export interface ISearchProvider { - searchModel(preferencesModel: ISettingsEditorModel): TPromise; + searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): TPromise; } export interface IKeybindingsEditor extends IEditor { @@ -48,6 +49,7 @@ export interface IKeybindingsEditor extends IEditor { activeKeybindingEntry: IKeybindingItemEntry; search(filter: string): void; + focusSearch(): void; clearSearchResults(): void; focusKeybindings(): void; defineKeybinding(keybindingEntry: IKeybindingItemEntry): TPromise; @@ -60,6 +62,7 @@ export interface IKeybindingsEditor extends IEditor { export const CONTEXT_SETTINGS_EDITOR = new RawContextKey('inSettingsEditor', false); export const CONTEXT_SETTINGS_SEARCH_FOCUS = new RawContextKey('inSettingsSearch', false); +export const CONTEXT_TOC_ROW_FOCUS = new RawContextKey('settingsTocRowFocus', false); export const CONTEXT_KEYBINDINGS_EDITOR = new RawContextKey('inKeybindings', false); export const CONTEXT_KEYBINDINGS_SEARCH_FOCUS = new RawContextKey('inKeybindingsSearch', false); export const CONTEXT_KEYBINDING_FOCUS = new RawContextKey('keybindingFocus', false); @@ -70,6 +73,10 @@ export const SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING = 'settings.action.focus export const SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING = 'settings.action.focusPreviousSetting'; export const SETTINGS_EDITOR_COMMAND_FOCUS_FILE = 'settings.action.focusSettingsFile'; export const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.editFocusedSetting'; +export const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; +export const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; +export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; + export const KEYBINDINGS_EDITOR_COMMAND_SEARCH = 'keybindings.editor.searchKeybindings'; export const KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'keybindings.editor.clearSearchResults'; export const KEYBINDINGS_EDITOR_COMMAND_DEFINE = 'keybindings.editor.defineKeybinding'; diff --git a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts index a5adc6ab4b7..98a861c6bbb 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts @@ -6,7 +6,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModel } from 'vs/editor/common/model'; import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -23,8 +23,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IEditorInput } from 'vs/workbench/common/editor'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { isEqual } from 'vs/base/common/paths'; import { isLinux } from 'vs/base/common/platform'; +import { isEqual } from 'vs/base/common/resources'; const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -66,14 +66,14 @@ export class PreferencesContribution implements IWorkbenchContribution { private onEditorOpening(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions, group: IEditorGroup): IOpenEditorOverride { const resource = editor.getResource(); if ( - !resource || resource.scheme !== 'file' || // require a file path opening - !endsWith(resource.fsPath, 'settings.json') || // file must end in settings.json + !resource || + !endsWith(resource.path, 'settings.json') || // resource must end in settings.json !this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING) // user has not disabled default settings editor ) { return void 0; } - // If the file resource was already opened before in the group, do not prevent + // If the resource was already opened before in the group, do not prevent // the opening of that resource. Otherwise we would have the same settings // opened twice (https://github.com/Microsoft/vscode/issues/36447) if (group.isOpened(editor)) { @@ -81,16 +81,16 @@ export class PreferencesContribution implements IWorkbenchContribution { } // Global User Settings File - if (isEqual(resource.fsPath, this.environmentService.appSettingsPath, !isLinux)) { - return { override: this.preferencesService.openGlobalSettings(options, group) }; + if (isEqual(resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) { + return { override: this.preferencesService.openGlobalSettings(true, options, group) }; } // Single Folder Workspace Settings File const state = this.workspaceService.getWorkbenchState(); if (state === WorkbenchState.FOLDER) { const folders = this.workspaceService.getWorkspace().folders; - if (resource.fsPath === folders[0].toResource(FOLDER_SETTINGS_PATH).fsPath) { - return { override: this.preferencesService.openWorkspaceSettings(options, group) }; + if (isEqual(resource, folders[0].toResource(FOLDER_SETTINGS_PATH))) { + return { override: this.preferencesService.openWorkspaceSettings(true, options, group) }; } } @@ -98,8 +98,8 @@ export class PreferencesContribution implements IWorkbenchContribution { else if (state === WorkbenchState.WORKSPACE) { const folders = this.workspaceService.getWorkspace().folders; for (let i = 0; i < folders.length; i++) { - if (resource.fsPath === folders[i].toResource(FOLDER_SETTINGS_PATH).fsPath) { - return { override: this.preferencesService.openFolderSettings(folders[i].uri, options, group) }; + if (isEqual(resource, folders[i].toResource(FOLDER_SETTINGS_PATH))) { + return { override: this.preferencesService.openFolderSettings(folders[i].uri, true, options, group) }; } } } diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index a9eaebd6a4c..06187cde93f 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -6,9 +6,9 @@ import 'vs/css!../browser/media/preferences'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { EditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; @@ -19,10 +19,10 @@ import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/prefer import { SettingsEditor2 } from 'vs/workbench/parts/preferences/browser/settingsEditor2'; import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { KeybindingsEditor } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; -import { OpenRawDefaultSettingsAction, OpenSettingsAction, OpenGlobalSettingsAction, OpenGlobalKeybindingsFileAction, OpenWorkspaceSettingsAction, OpenFolderSettingsAction, ConfigureLanguageBasedSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OpenGlobalKeybindingsAction, OpenSettings2Action } from 'vs/workbench/parts/preferences/browser/preferencesActions'; +import { OpenDefaultKeybindingsFileAction, OpenRawDefaultSettingsAction, OpenSettingsAction, OpenGlobalSettingsAction, OpenGlobalKeybindingsFileAction, OpenWorkspaceSettingsAction, OpenFolderSettingsAction, ConfigureLanguageBasedSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OpenGlobalKeybindingsAction, OpenSettings2Action, OpenSettingsJsonAction } from 'vs/workbench/parts/preferences/browser/preferencesActions'; import { IKeybindingsEditor, IPreferencesSearchService, CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_SEARCH, - KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS + KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SEARCH, CONTEXT_SETTINGS_EDITOR, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, CONTEXT_SETTINGS_SEARCH_FOCUS, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, CONTEXT_TOC_ROW_FOCUS, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/parts/preferences/common/preferences'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -34,6 +34,8 @@ import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } fro import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { PreferencesSearchService } from 'vs/workbench/parts/preferences/electron-browser/preferencesSearch'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { Command } from 'vs/editor/browser/editorExtensions'; +import { Context as SuggestContext } from 'vs/editor/contrib/suggest/suggest'; registerSingleton(IPreferencesSearchService, PreferencesSearchService); @@ -147,17 +149,23 @@ class KeybindingsEditorInputFactory implements IEditorInputFactory { } } -class SettingsEditor2InputFactory implements IEditorInputFactory { - - public serialize(editorInput: SettingsEditor2Input): string { - return JSON.stringify({}); - } - - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - return instantiationService.createInstance(SettingsEditor2Input); - } +interface ISerializedSettingsEditor2EditorInput { } +class SettingsEditor2InputFactory implements IEditorInputFactory { + + public serialize(input: SettingsEditor2Input): string { + const serialized: ISerializedSettingsEditor2EditorInput = { + }; + + return JSON.stringify(serialized); + } + + public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SettingsEditor2Input { + return instantiationService.createInstance( + SettingsEditor2Input); + } +} interface ISerializedDefaultPreferencesEditorInput { resource: string; @@ -191,15 +199,18 @@ const category = nls.localize('preferences', "Preferences"); const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawDefaultSettingsAction, OpenRawDefaultSettingsAction.ID, OpenRawDefaultSettingsAction.LABEL), 'Preferences: Open Raw Default Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL), 'Preferences: Open Settings (Preview)', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsJsonAction, OpenSettingsJsonAction.ID, OpenSettingsJsonAction.LABEL), 'Preferences: Open Settings (JSON)', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL), 'Preferences: Open Settings (UI)', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL), 'Preferences: Open User Settings', category); + registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsAction, OpenGlobalKeybindingsAction.ID, OpenGlobalKeybindingsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) }), 'Preferences: Open Keyboard Shortcuts', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDefaultKeybindingsFileAction, OpenDefaultKeybindingsFileAction.ID, OpenDefaultKeybindingsFileAction.LABEL), 'Preferences: Open Default Keyboard Shortcuts File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsFileAction, OpenGlobalKeybindingsFileAction.ID, OpenGlobalKeybindingsFileAction.LABEL, { primary: null }), 'Preferences: Open Keyboard Shortcuts File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ConfigureLanguageBasedSettingsAction, ConfigureLanguageBasedSettingsAction.ID, ConfigureLanguageBasedSettingsAction.LABEL), 'Preferences: Configure Language Specific Settings...', category); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_DEFINE, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), handler: (accessor, args: any) => { @@ -210,7 +221,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_REMOVE, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyCode.Delete, mac: { @@ -224,7 +235,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_RESET, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: null, handler: (accessor, args: any) => { @@ -235,15 +246,15 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_SEARCH, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyMod.CtrlCmd | KeyCode.KEY_F, - handler: (accessor, args: any) => (accessor.get(IEditorService).activeControl as IKeybindingsEditor).search('') + handler: (accessor, args: any) => (accessor.get(IEditorService).activeControl as IKeybindingsEditor).focusSearch() }); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: null, handler: (accessor, args: any) => { @@ -254,7 +265,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_COPY, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyMod.CtrlCmd | KeyCode.KEY_C, handler: (accessor, args: any) => { @@ -265,7 +276,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: null, handler: (accessor, args: any) => { @@ -276,7 +287,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), primary: KeyCode.DownArrow, handler: (accessor, args: any) => { @@ -287,7 +298,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), primary: KeyCode.Escape, handler: (accessor, args: any) => { @@ -323,4 +334,174 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: `${category}: ${OpenWorkspaceSettingsAction.LABEL}`, }, when: new RawContextKey('workbenchState', '').notEqualsTo('empty') -}); \ No newline at end of file +}); + +abstract class SettingsCommand extends Command { + + protected getPreferencesEditor(accessor: ServicesAccessor): PreferencesEditor | SettingsEditor2 { + const activeControl = accessor.get(IEditorService).activeControl; + if (activeControl instanceof PreferencesEditor || activeControl instanceof SettingsEditor2) { + return activeControl; + } + + return null; + } + +} +class StartSearchDefaultSettingsCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.focusSearch(); + } + } +} +const startSearchCommand = new StartSearchDefaultSettingsCommand({ + id: SETTINGS_EDITOR_COMMAND_SEARCH, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR), + kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_F, weight: KeybindingWeight.EditorContrib } +}); +startSearchCommand.register(); + +class ClearSearchResultsCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.clearSearchResults(); + } + } +} +const clearSearchResultsCommand = new ClearSearchResultsCommand({ + id: SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, + precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + kbOpts: { primary: KeyCode.Escape, weight: KeybindingWeight.EditorContrib } +}); +clearSearchResultsCommand.register(); + +class FocusSettingsFileEditorCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof PreferencesEditor) { + preferencesEditor.focusSettingsFileEditor(); + } else { + preferencesEditor.focusSettings(); + } + } +} +const focusSettingsFileEditorCommand = new FocusSettingsFileEditorCommand({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_FILE, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_SEARCH_FOCUS, SuggestContext.Visible.toNegated()), + kbOpts: { primary: KeyCode.DownArrow, weight: KeybindingWeight.EditorContrib } +}); +focusSettingsFileEditorCommand.register(); + +const focusSettingsFromSearchCommand = new FocusSettingsFileEditorCommand({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_SEARCH_FOCUS, SuggestContext.Visible.toNegated()), + kbOpts: { primary: KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib } +}); +focusSettingsFromSearchCommand.register(); + +class FocusNextSearchResultCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof PreferencesEditor) { + preferencesEditor.focusNextResult(); + } + } +} +const focusNextSearchResultCommand = new FocusNextSearchResultCommand({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, + precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + kbOpts: { primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib } +}); +focusNextSearchResultCommand.register(); + +class FocusPreviousSearchResultCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof PreferencesEditor) { + preferencesEditor.focusPreviousResult(); + } + } +} +const focusPreviousSearchResultCommand = new FocusPreviousSearchResultCommand({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, + precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + kbOpts: { primary: KeyMod.Shift | KeyCode.Enter, weight: KeybindingWeight.EditorContrib } +}); +focusPreviousSearchResultCommand.register(); + +class EditFocusedSettingCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof PreferencesEditor) { + preferencesEditor.editFocusedPreference(); + } + } +} +const editFocusedSettingCommand = new EditFocusedSettingCommand({ + id: SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, + precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.US_DOT, weight: KeybindingWeight.EditorContrib } +}); +editFocusedSettingCommand.register(); + +class FocusSettingsListCommand extends SettingsCommand { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof SettingsEditor2) { + preferencesEditor.focusSettings(); + } + } +} + +const focusSettingsListCommand = new FocusSettingsListCommand({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS), + kbOpts: { primary: KeyCode.Enter, weight: KeybindingWeight.WorkbenchContrib } +}); +focusSettingsListCommand.register(); + +class ShowContextMenuCommand extends SettingsCommand { + public runCommand(accessor: ServicesAccessor, args: any): void { + const preferencesEditor = this.getPreferencesEditor(accessor); + if (preferencesEditor instanceof SettingsEditor2) { + preferencesEditor.showContextMenu(); + } + } +} + +const showContextMenuCommand = new ShowContextMenuCommand({ + id: SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR), + kbOpts: { primary: KeyMod.Shift | KeyCode.F9, weight: KeybindingWeight.WorkbenchContrib } +}); +showContextMenuCommand.register(); + +// Preferences menu + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '1_settings', + command: { + id: OpenSettingsAction.ID, + title: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: OpenGlobalKeybindingsAction.ID, + title: nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 3b40c62f656..9ff55ccdc9c 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -21,6 +21,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { IPreferencesSearchService, ISearchProvider, IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; export interface IEndpointDetails { urlBase: string; @@ -90,6 +92,9 @@ export class PreferencesSearchService extends Disposable implements IPreferences } export class LocalSearchProvider implements ISearchProvider { + static readonly EXACT_MATCH_SCORE = 10000; + static readonly START_SCORE = 1000; + constructor(private _filter: string) { // Remove " and : which are likely to be copypasted as part of a setting name. // Leave other special characters which the user might want to search for. @@ -99,30 +104,41 @@ export class LocalSearchProvider implements ISearchProvider { .trim(); } - searchModel(preferencesModel: ISettingsEditorModel): TPromise { + searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): TPromise { if (!this._filter) { return TPromise.wrap(null); } - let score = 1000; // Sort is not stable + let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable const settingMatcher = (setting: ISetting) => { const matches = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + const score = this._filter === setting.key ? + LocalSearchProvider.EXACT_MATCH_SCORE : + orderedScore--; + return matches && matches.length ? { matches, - score: score-- + score } : null; }; const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher); - return TPromise.wrap({ - filterMatches - }); + if (filterMatches[0] && filterMatches[0].score === LocalSearchProvider.EXACT_MATCH_SCORE) { + return TPromise.wrap({ + filterMatches: filterMatches.slice(0, 1), + exactMatch: true + }); + } else { + return TPromise.wrap({ + filterMatches + }); + } } private getGroupFilter(filter: string): IGroupFilter { - const regex = strings.createRegExp(this._filter, false, { global: true }); + const regex = strings.createRegExp(filter, false, { global: true }); return (group: ISettingsGroup) => { return regex.test(group.title); }; @@ -144,8 +160,9 @@ interface IBingRequestDetails { class RemoteSearchProvider implements ISearchProvider { // Must keep extension filter size under 8kb. 42 filters puts us there. - private static MAX_REQUEST_FILTERS = 42; - private static MAX_REQUESTS = 10; + private static readonly MAX_REQUEST_FILTERS = 42; + private static readonly MAX_REQUESTS = 10; + private static readonly NEW_EXTENSIONS_MIN_SCORE = 1; private _remoteSearchP: TPromise; @@ -155,36 +172,50 @@ class RemoteSearchProvider implements ISearchProvider { @ILogService private logService: ILogService ) { this._remoteSearchP = this.options.filter ? - this.getSettingsForFilter(this.options.filter) : + TPromise.wrap(this.getSettingsForFilter(this.options.filter)) : TPromise.wrap(null); } - searchModel(preferencesModel: ISettingsEditorModel): TPromise { + searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): TPromise { return this._remoteSearchP.then(remoteResult => { if (!remoteResult) { return null; } + if (token && token.isCancellationRequested) { + throw canceled(); + } + const resultKeys = Object.keys(remoteResult.scoredResults); const highScoreKey = top(resultKeys, (a, b) => remoteResult.scoredResults[b].score - remoteResult.scoredResults[a].score, 1)[0]; const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey].score : 0; const minScore = highScore / 5; if (this.options.newExtensionsOnly) { - const passingScoreKeys = resultKeys.filter(k => remoteResult.scoredResults[k].score >= minScore); - const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { - const remoteSetting = remoteResult.scoredResults[k]; - const setting = remoteSettingToISetting(remoteSetting); - return { - setting, - score: remoteSetting.score, - matches: [] // TODO + return this.installedExtensions.then(installedExtensions => { + const newExtsMinScore = Math.max(RemoteSearchProvider.NEW_EXTENSIONS_MIN_SCORE, minScore); + const passingScoreKeys = resultKeys + .filter(k => { + const result = remoteResult.scoredResults[k]; + const resultExtId = (result.extensionPublisher + '.' + result.extensionName).toLowerCase(); + return !installedExtensions.some(ext => ext.galleryIdentifier.id.toLowerCase() === resultExtId); + }) + .filter(k => remoteResult.scoredResults[k].score >= newExtsMinScore); + + const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { + const remoteSetting = remoteResult.scoredResults[k]; + const setting = remoteSettingToISetting(remoteSetting); + return { + setting, + score: remoteSetting.score, + matches: [] // TODO + }; + }); + + return { + filterMatches, + metadata: remoteResult }; }); - - return { - filterMatches, - metadata: remoteResult - }; } else { const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher); @@ -196,7 +227,7 @@ class RemoteSearchProvider implements ISearchProvider { }); } - private async getSettingsForFilter(filter: string): TPromise { + private async getSettingsForFilter(filter: string): Promise { const allRequestDetails: IBingRequestDetails[] = []; // Only send MAX_REQUESTS requests in total just to keep it sane @@ -240,7 +271,7 @@ class RemoteSearchProvider implements ISearchProvider { 'api-key': this.options.endpoint.key }, timeout: 5000 - }).then(context => { + }, CancellationToken.None).then(context => { if (context.res.statusCode >= 300) { throw new Error(`${details} returned status code: ${context.res.statusCode}`); } @@ -308,7 +339,7 @@ class RemoteSearchProvider implements ISearchProvider { }; } - private async prepareRequest(query: string, filterPage = 0): TPromise { + private async prepareRequest(query: string, filterPage = 0): Promise { const verbatimQuery = query; query = escapeSpecialChars(query); const boost = 10; @@ -391,6 +422,7 @@ function escapeSpecialChars(query: string): string { function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting { return { description: remoteSetting.description.split('\n'), + descriptionIsMarkdown: false, descriptionRanges: null, key: remoteSetting.key, keyRange: null, @@ -501,6 +533,16 @@ class SettingMatches { } private toKeyRange(setting: ISetting, match: IMatch): IRange { + if (!setting.keyRange) { + // No source range? Return fake range, don't care + return { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + }; + } + return { startLineNumber: setting.keyRange.startLineNumber, startColumn: setting.keyRange.startColumn + match.start, @@ -510,6 +552,16 @@ class SettingMatches { } private toDescriptionRange(setting: ISetting, match: IMatch, lineIndex: number): IRange { + if (!setting.keyRange) { + // No source range? Return fake range, don't care + return { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + }; + } + return { startLineNumber: setting.descriptionRanges[lineIndex].startLineNumber, startColumn: setting.descriptionRanges[lineIndex].startColumn + match.start, @@ -519,6 +571,16 @@ class SettingMatches { } private toValueRange(setting: ISetting, match: IMatch): IRange { + if (!setting.keyRange) { + // No source range? Return fake range, don't care + return { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + }; + } + return { startLineNumber: setting.valueRange.startLineNumber, startColumn: setting.valueRange.startColumn + match.start + 1, @@ -526,4 +588,4 @@ class SettingMatches { endColumn: setting.valueRange.startColumn + match.end + 1 }; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/preferences/test/browser/settingsTree.test.ts b/src/vs/workbench/parts/preferences/test/browser/settingsTree.test.ts deleted file mode 100644 index a81ed4ab187..00000000000 --- a/src/vs/workbench/parts/preferences/test/browser/settingsTree.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import { settingKeyToDisplayFormat } from 'vs/workbench/parts/preferences/browser/settingsTree'; - -suite('SettingsTree', () => { - test('settingKeyToDisplayFormat', () => { - assert.deepEqual( - settingKeyToDisplayFormat('foo.bar'), - { - category: 'Foo', - label: 'Bar' - }); - - assert.deepEqual( - settingKeyToDisplayFormat('foo.bar.etc'), - { - category: 'Foo.Bar', - label: 'Etc' - }); - - assert.deepEqual( - settingKeyToDisplayFormat('fooBar.etcSomething'), - { - category: 'Foo Bar', - label: 'Etc Something' - }); - - assert.deepEqual( - settingKeyToDisplayFormat('foo'), - { - category: '', - label: 'Foo' - }); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/test/browser/settingsTreeModels.test.ts b/src/vs/workbench/parts/preferences/test/browser/settingsTreeModels.test.ts new file mode 100644 index 00000000000..122f23a970f --- /dev/null +++ b/src/vs/workbench/parts/preferences/test/browser/settingsTreeModels.test.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { settingKeyToDisplayFormat, parseQuery, IParsedQuery } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; + +suite('SettingsTree', () => { + test('settingKeyToDisplayFormat', () => { + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar'), + { + category: 'Foo', + label: 'Bar' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar.etc'), + { + category: 'Foo › Bar', + label: 'Etc' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('fooBar.etcSomething'), + { + category: 'Foo Bar', + label: 'Etc Something' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('foo'), + { + category: '', + label: 'Foo' + }); + }); + + test('settingKeyToDisplayFormat - with category', () => { + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar', 'foo'), + { + category: '', + label: 'Bar' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('disableligatures.ligatures', 'disableligatures'), + { + category: '', + label: 'Ligatures' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar.etc', 'foo'), + { + category: 'Bar', + label: 'Etc' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('fooBar.etcSomething', 'foo'), + { + category: 'Foo Bar', + label: 'Etc Something' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar.etc', 'foo/bar'), + { + category: '', + label: 'Etc' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('foo.bar.etc', 'something/foo'), + { + category: 'Bar', + label: 'Etc' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('bar.etc', 'something.bar'), + { + category: '', + label: 'Etc' + }); + + assert.deepEqual( + settingKeyToDisplayFormat('fooBar.etc', 'fooBar'), + { + category: '', + label: 'Etc' + }); + + + assert.deepEqual( + settingKeyToDisplayFormat('fooBar.somethingElse.etc', 'fooBar'), + { + category: 'Something Else', + label: 'Etc' + }); + }); + + test('parseQuery', () => { + function testParseQuery(input: string, expected: IParsedQuery) { + assert.deepEqual( + parseQuery(input), + expected, + input + ); + } + + testParseQuery( + '', + { + tags: [], + query: '' + }); + + testParseQuery( + '@modified', + { + tags: ['modified'], + query: '' + }); + + testParseQuery( + '@tag:foo', + { + tags: ['foo'], + query: '' + }); + + testParseQuery( + '@modified foo', + { + tags: ['modified'], + query: 'foo' + }); + + testParseQuery( + '@tag:foo @modified', + { + tags: ['foo', 'modified'], + query: '' + }); + + testParseQuery( + '@tag:foo @modified my query', + { + tags: ['foo', 'modified'], + query: 'my query' + }); + + testParseQuery( + 'test @modified query', + { + tags: ['modified'], + query: 'test query' + }); + + testParseQuery( + 'test @modified', + { + tags: ['modified'], + query: 'test' + }); + + testParseQuery( + 'query has @ for some reason', + { + tags: [], + query: 'query has @ for some reason' + }); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts b/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts index 0e1dd8af193..f3a7c6974e6 100644 --- a/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/commandsHandler.ts @@ -33,6 +33,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { isPromiseCanceledError } from 'vs/base/common/errors'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export const ALL_COMMANDS_PREFIX = '>'; @@ -58,7 +60,7 @@ function resolveCommandHistory(configurationService: IConfigurationService): num class CommandsHistory { - public static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50; + static readonly DEFAULT_COMMANDS_HISTORY_LENGTH = 50; private static readonly PREF_KEY_CACHE = 'commandPalette.mru.cache'; private static readonly PREF_KEY_COUNTER = 'commandPalette.mru.counter'; @@ -105,6 +107,7 @@ class CommandsHistory { } entries.forEach(entry => commandHistory.set(entry.key, entry.value)); } + commandCounter = this.storageService.getInteger(CommandsHistory.PREF_KEY_COUNTER, void 0, commandCounter); } @@ -114,26 +117,26 @@ class CommandsHistory { } private save(): void { - let serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] }; + const serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] }; commandHistory.forEach((value, key) => serializedCache.entries.push({ key, value })); + this.storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache)); this.storageService.store(CommandsHistory.PREF_KEY_COUNTER, commandCounter); } - public push(commandId: string): void { - // set counter to command - commandHistory.set(commandId, commandCounter++); + push(commandId: string): void { + commandHistory.set(commandId, commandCounter++); // set counter to command } - public peek(commandId: string): number { + peek(commandId: string): number { return commandHistory.peek(commandId); } } export class ShowAllCommandsAction extends Action { - public static readonly ID = 'workbench.action.showCommands'; - public static readonly LABEL = nls.localize('showTriggerActions', "Show All Commands"); + static readonly ID = 'workbench.action.showCommands'; + static readonly LABEL = nls.localize('showTriggerActions', "Show All Commands"); constructor( id: string, @@ -144,7 +147,7 @@ export class ShowAllCommandsAction extends Action { super(id, label); } - public run(context?: any): TPromise { + run(context?: any): TPromise { const config = this.configurationService.getValue(); const restoreInput = config.workbench && config.workbench.commandPalette && config.workbench.commandPalette.preserveInput === true; @@ -162,8 +165,8 @@ export class ShowAllCommandsAction extends Action { export class ClearCommandHistoryAction extends Action { - public static readonly ID = 'workbench.action.clearCommandHistory'; - public static readonly LABEL = nls.localize('clearCommandHistory', "Clear Command History"); + static readonly ID = 'workbench.action.clearCommandHistory'; + static readonly LABEL = nls.localize('clearCommandHistory', "Clear Command History"); constructor( id: string, @@ -173,7 +176,7 @@ export class ClearCommandHistoryAction extends Action { super(id, label); } - public run(context?: any): TPromise { + run(context?: any): TPromise { const commandHistoryLength = resolveCommandHistory(this.configurationService); if (commandHistoryLength > 0) { commandHistory = new LRUCache(commandHistoryLength); @@ -199,7 +202,7 @@ class CommandPaletteEditorAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): TPromise { + run(accessor: ServicesAccessor, editor: ICodeEditor): TPromise { const quickOpenService = accessor.get(IQuickOpenService); // Show with prefix @@ -239,35 +242,35 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup { this.setHighlights(highlights.label, null, highlights.alias); } - public getCommandId(): string { + getCommandId(): string { return this.commandId; } - public getLabel(): string { + getLabel(): string { return this.label; } - public getSortLabel(): string { + getSortLabel(): string { return this.labelLowercase; } - public getDescription(): string { + getDescription(): string { return this.description; } - public setDescription(description: string): void { + setDescription(description: string): void { this.description = description; } - public getKeybinding(): ResolvedKeybinding { + getKeybinding(): ResolvedKeybinding { return this.keybinding; } - public getDetail(): string { + getDetail(): string { return this.alias; } - public getAriaLabel(): string { + getAriaLabel(): string { if (this.keybindingAriaLabel) { return nls.localize('entryAriaLabelWithKey', "{0}, {1}, commands", this.getLabel(), this.keybindingAriaLabel); } @@ -275,7 +278,7 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup { return nls.localize('entryAriaLabel', "{0}, commands", this.getLabel()); } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { this.runAction(this.getAction()); @@ -293,7 +296,7 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup { this.onBeforeRun(this.commandId); // Use a timeout to give the quick open widget a chance to close itself first - TPromise.timeout(50).done(() => { + setTimeout(() => { if (action && (!(action instanceof Action) || action.enabled)) { try { /* __GDPR__ @@ -303,7 +306,7 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup { } */ this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'quick open' }); - (action.run() || TPromise.as(null)).done(() => { + (action.run() || TPromise.as(null)).then(() => { if (action instanceof Action) { action.dispose(); } @@ -314,7 +317,7 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup { } else { this.notificationService.info(nls.localize('actionNotEnabled', "Command '{0}' is not enabled in the current context.", this.getLabel())); } - }, err => this.onError(err)); + }, 50); } private onError(error?: Error): void { @@ -372,7 +375,7 @@ const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString); export class CommandsHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.commands'; + static readonly ID = 'workbench.picker.commands'; private lastSearchValue: string; private commandHistoryEnabled: boolean; @@ -383,7 +386,8 @@ export class CommandsHandler extends QuickOpenHandler { @IInstantiationService private instantiationService: IInstantiationService, @IKeybindingService private keybindingService: IKeybindingService, @IMenuService private menuService: IMenuService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IExtensionService private extensionService: IExtensionService ) { super(); @@ -397,78 +401,87 @@ export class CommandsHandler extends QuickOpenHandler { this.commandHistoryEnabled = resolveCommandHistory(this.configurationService) > 0; } - public getResults(searchValue: string): TPromise { - searchValue = searchValue.trim(); - this.lastSearchValue = searchValue; + getResults(searchValue: string, token: CancellationToken): TPromise { - // Editor Actions - const activeTextEditorWidget = this.editorService.activeTextEditorWidget; - let editorActions: IEditorAction[] = []; - if (activeTextEditorWidget && types.isFunction(activeTextEditorWidget.getSupportedActions)) { - editorActions = activeTextEditorWidget.getSupportedActions(); - } - - const editorEntries = this.editorActionsToEntries(editorActions, searchValue); - - // Other Actions - const menu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.CommandPalette, accessor.get(IContextKeyService))); - const menuActions = menu.getActions().reduce((r, [, actions]) => [...r, ...actions], []); - const commandEntries = this.menuItemActionsToEntries(menuActions, searchValue); - - // Concat - let entries = [...editorEntries, ...commandEntries]; - - // Remove duplicates - entries = arrays.distinct(entries, entry => `${entry.getLabel()}${entry.getGroupLabel()}${entry.getCommandId()}`); - - // Handle label clashes - const commandLabels = new Set(); - entries.forEach(entry => { - const commandLabel = `${entry.getLabel()}${entry.getGroupLabel()}`; - if (commandLabels.has(commandLabel)) { - entry.setDescription(entry.getCommandId()); - } else { - commandLabels.add(commandLabel); - } - }); - - // Sort by MRU order and fallback to name otherwie - entries = entries.sort((elementA, elementB) => { - const counterA = this.commandsHistory.peek(elementA.getCommandId()); - const counterB = this.commandsHistory.peek(elementB.getCommandId()); - - if (counterA && counterB) { - return counterA > counterB ? -1 : 1; // use more recently used command before older + // wait for extensions being registered to cover all commands + // also from extensions + return this.extensionService.whenInstalledExtensionsRegistered().then(() => { + if (token.isCancellationRequested) { + return new QuickOpenModel([]); } - if (counterA) { - return -1; // first command was used, so it wins over the non used one + searchValue = searchValue.trim(); + this.lastSearchValue = searchValue; + + // Editor Actions + const activeTextEditorWidget = this.editorService.activeTextEditorWidget; + let editorActions: IEditorAction[] = []; + if (activeTextEditorWidget && types.isFunction(activeTextEditorWidget.getSupportedActions)) { + editorActions = activeTextEditorWidget.getSupportedActions(); } - if (counterB) { - return 1; // other command was used so it wins over the command - } + const editorEntries = this.editorActionsToEntries(editorActions, searchValue); - // both commands were never used, so we sort by name - return elementA.getSortLabel().localeCompare(elementB.getSortLabel()); - }); + // Other Actions + const menu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.CommandPalette, accessor.get(IContextKeyService))); + const menuActions = menu.getActions().reduce((r, [, actions]) => [...r, ...actions], []).filter(action => action instanceof MenuItemAction) as MenuItemAction[]; + const commandEntries = this.menuItemActionsToEntries(menuActions, searchValue); - // Introduce group marker border between recently used and others - // only if we have recently used commands in the result set - const firstEntry = entries[0]; - if (firstEntry && this.commandsHistory.peek(firstEntry.getCommandId())) { - firstEntry.setGroupLabel(nls.localize('recentlyUsed', "recently used")); - for (let i = 1; i < entries.length; i++) { - const entry = entries[i]; - if (!this.commandsHistory.peek(entry.getCommandId())) { - entry.setShowBorder(true); - entry.setGroupLabel(nls.localize('morecCommands', "other commands")); - break; + // Concat + let entries = [...editorEntries, ...commandEntries]; + + // Remove duplicates + entries = arrays.distinct(entries, entry => `${entry.getLabel()}${entry.getGroupLabel()}${entry.getCommandId()}`); + + // Handle label clashes + const commandLabels = new Set(); + entries.forEach(entry => { + const commandLabel = `${entry.getLabel()}${entry.getGroupLabel()}`; + if (commandLabels.has(commandLabel)) { + entry.setDescription(entry.getCommandId()); + } else { + commandLabels.add(commandLabel); + } + }); + + // Sort by MRU order and fallback to name otherwie + entries = entries.sort((elementA, elementB) => { + const counterA = this.commandsHistory.peek(elementA.getCommandId()); + const counterB = this.commandsHistory.peek(elementB.getCommandId()); + + if (counterA && counterB) { + return counterA > counterB ? -1 : 1; // use more recently used command before older + } + + if (counterA) { + return -1; // first command was used, so it wins over the non used one + } + + if (counterB) { + return 1; // other command was used so it wins over the command + } + + // both commands were never used, so we sort by name + return elementA.getSortLabel().localeCompare(elementB.getSortLabel()); + }); + + // Introduce group marker border between recently used and others + // only if we have recently used commands in the result set + const firstEntry = entries[0]; + if (firstEntry && this.commandsHistory.peek(firstEntry.getCommandId())) { + firstEntry.setGroupLabel(nls.localize('recentlyUsed', "recently used")); + for (let i = 1; i < entries.length; i++) { + const entry = entries[i]; + if (!this.commandsHistory.peek(entry.getCommandId())) { + entry.setShowBorder(true); + entry.setGroupLabel(nls.localize('morecCommands', "other commands")); + break; + } } } - } - return TPromise.as(new QuickOpenModel(entries)); + return new QuickOpenModel(entries); + }); } private editorActionsToEntries(actions: IEditorAction[], searchValue: string): EditorActionCommandEntry[] { @@ -540,7 +553,7 @@ export class CommandsHandler extends QuickOpenHandler { return entries; } - public getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { + getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { let autoFocusPrefixMatch = searchValue.trim(); if (autoFocusPrefixMatch && this.commandHistoryEnabled) { @@ -556,11 +569,11 @@ export class CommandsHandler extends QuickOpenHandler { }; } - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { return nls.localize('noCommandsMatching', "No commands matching"); } - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { if (canceled) { lastCommandPaletteInput = void 0; // clear last input when user canceled quick open } diff --git a/src/vs/workbench/parts/quickopen/browser/gotoLineHandler.ts b/src/vs/workbench/parts/quickopen/browser/gotoLineHandler.ts index d858f369bd9..dc35c9b2a9b 100644 --- a/src/vs/workbench/parts/quickopen/browser/gotoLineHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/gotoLineHandler.ts @@ -22,13 +22,15 @@ import { IEditorOptions, RenderLineNumbersType } from 'vs/editor/common/config/e import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { once } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const GOTO_LINE_PREFIX = ':'; export class GotoLineAction extends QuickOpenAction { - public static readonly ID = 'workbench.action.gotoLine'; - public static readonly LABEL = nls.localize('gotoLine', "Go to Line..."); + static readonly ID = 'workbench.action.gotoLine'; + static readonly LABEL = nls.localize('gotoLine', "Go to Line..."); constructor(actionId: string, actionLabel: string, @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, @@ -37,7 +39,7 @@ export class GotoLineAction extends QuickOpenAction { super(actionId, actionLabel, GOTO_LINE_PREFIX, _quickOpenService); } - public run(): TPromise { + run(): TPromise { let activeTextEditorWidget = this.editorService.activeTextEditorWidget; if (isDiffEditor(activeTextEditorWidget)) { @@ -60,12 +62,7 @@ export class GotoLineAction extends QuickOpenAction { const result = super.run(); if (restoreOptions) { - let toDispose = this._quickOpenService.onHide(() => { - if (!toDispose) { - return; - } - toDispose.dispose(); - toDispose = null; + once(this._quickOpenService.onHide)(() => { activeTextEditorWidget.updateOptions(restoreOptions); }); } @@ -92,7 +89,7 @@ class GotoLineEntry extends EditorQuickOpenEntry { this.column = numbers[1]; } - public getLabel(): string { + getLabel(): string { // Inform user about valid range if input is invalid const maxLineNumber = this.getMaxLineNumber(); @@ -123,7 +120,7 @@ class GotoLineEntry extends EditorQuickOpenEntry { return model && types.isFunction((model).getLineCount) ? (model).getLineCount() : -1; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { return this.runOpen(context); } @@ -131,18 +128,18 @@ class GotoLineEntry extends EditorQuickOpenEntry { return this.runPreview(); } - public getInput(): IEditorInput { + getInput(): IEditorInput { return this.editorService.activeEditor; } - public getOptions(pinned?: boolean): ITextEditorOptions { + getOptions(pinned?: boolean): ITextEditorOptions { return { selection: this.toSelection(), pinned }; } - public runOpen(context: IEntryRunContext): boolean { + runOpen(context: IEntryRunContext): boolean { // No-op if range is not valid if (this.invalidRange()) { @@ -166,7 +163,7 @@ class GotoLineEntry extends EditorQuickOpenEntry { return true; } - public runPreview(): boolean { + runPreview(): boolean { // No-op if range is not valid if (this.invalidRange()) { @@ -208,7 +205,7 @@ interface IEditorLineDecoration { export class GotoLineHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.line'; + static readonly ID = 'workbench.picker.line'; private rangeHighlightDecorationId: IEditorLineDecoration; private lastKnownEditorViewState: IEditorViewState; @@ -217,11 +214,11 @@ export class GotoLineHandler extends QuickOpenHandler { super(); } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('gotoLineHandlerAriaLabel', "Type a line number to navigate to."); } - public getResults(searchValue: string): TPromise { + getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); // Remember view state to be able to restore on cancel @@ -233,13 +230,13 @@ export class GotoLineHandler extends QuickOpenHandler { return TPromise.as(new QuickOpenModel([new GotoLineEntry(searchValue, this.editorService, this)])); } - public canRun(): boolean | string { + canRun(): boolean | string { const canRun = !!this.editorService.activeTextEditorWidget; return canRun ? true : nls.localize('cannotRunGotoLine', "Open a text file first to go to a line"); } - public decorateOutline(range: IRange, editor: IEditor, group: IEditorGroup): void { + decorateOutline(range: IRange, editor: IEditor, group: IEditorGroup): void { editor.changeDecorations(changeAccessor => { const deleteDecorations: string[] = []; @@ -284,7 +281,7 @@ export class GotoLineHandler extends QuickOpenHandler { }); } - public clearDecorations(): void { + clearDecorations(): void { if (this.rangeHighlightDecorationId) { this.editorService.visibleControls.forEach(editor => { if (editor.group.id === this.rangeHighlightDecorationId.groupId) { @@ -302,7 +299,7 @@ export class GotoLineHandler extends QuickOpenHandler { } } - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { // Clear Highlight Decorations if present this.clearDecorations(); @@ -318,7 +315,7 @@ export class GotoLineHandler extends QuickOpenHandler { this.lastKnownEditorViewState = null; } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { return { autoFocusFirstEntry: searchValue.trim().length > 0 }; diff --git a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts index 5c9f7406346..1befbcbc0e7 100644 --- a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts @@ -5,7 +5,7 @@ 'use strict'; -import 'vs/css!./media/gotoSymbolHandler'; +import 'vs/css!vs/editor/contrib/documentSymbols/media/symbol-icons'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; @@ -19,21 +19,45 @@ import { IModelDecorationsChangeAccessor, OverviewRulerLane, IModelDeltaDecorati import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { DocumentSymbolProviderRegistry, SymbolInformation, symbolKindToCssClass } from 'vs/editor/common/modes'; +import { DocumentSymbolProviderRegistry, DocumentSymbol, symbolKindToCssClass } from 'vs/editor/common/modes'; import { IRange } from 'vs/editor/common/core/range'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; import { GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { asThenable } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export const GOTO_SYMBOL_PREFIX = '@'; export const SCOPE_PREFIX = ':'; +const NLS_SYMBOL_CACHE: { [type: string]: string } = { + 'method': nls.localize('method', "methods ({0})"), + 'function': nls.localize('function', "functions ({0})"), + 'constructor': nls.localize('_constructor', "constructors ({0})"), + 'variable': nls.localize('variable', "variables ({0})"), + 'class': nls.localize('class', "classes ({0})"), + 'interface': nls.localize('interface', "interfaces ({0})"), + 'namespace': nls.localize('namespace', "namespaces ({0})"), + 'package': nls.localize('package', "packages ({0})"), + 'module': nls.localize('modules', "modules ({0})"), + 'property': nls.localize('property', "properties ({0})"), + 'enum': nls.localize('enum', "enumerations ({0})"), + 'string': nls.localize('string', "strings ({0})"), + 'rule': nls.localize('rule', "rules ({0})"), + 'file': nls.localize('file', "files ({0})"), + 'array': nls.localize('array', "arrays ({0})"), + 'number': nls.localize('number', "numbers ({0})"), + 'boolean': nls.localize('boolean', "booleans ({0})"), + 'object': nls.localize('object', "objects ({0})"), + 'key': nls.localize('key', "keys ({0})") +}; + export class GotoSymbolAction extends QuickOpenAction { - public static readonly ID = 'workbench.action.gotoSymbol'; - public static readonly LABEL = nls.localize('gotoSymbol', "Go to Symbol in File..."); + static readonly ID = 'workbench.action.gotoSymbol'; + static readonly LABEL = nls.localize('gotoSymbol', "Go to Symbol in File..."); constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) { super(actionId, actionLabel, GOTO_SYMBOL_PREFIX, quickOpenService); @@ -41,15 +65,8 @@ export class GotoSymbolAction extends QuickOpenAction { } class OutlineModel extends QuickOpenModel { - private outline: Outline; - constructor(outline: Outline, entries: SymbolEntry[]) { - super(entries); - - this.outline = outline; - } - - public applyFilter(searchValue: string): void { + applyFilter(searchValue: string): void { // Normalize search let normalizedSearchValue = searchValue; @@ -107,7 +124,7 @@ class OutlineModel extends QuickOpenModel { // Update previous result with count if (currentResult) { - currentResult.setGroupLabel(this.renderGroupLabel(currentType, typeCounter, this.outline)); + currentResult.setGroupLabel(this.renderGroupLabel(currentType, typeCounter)); } currentType = result.getType(); @@ -125,7 +142,7 @@ class OutlineModel extends QuickOpenModel { // Update previous result with count if (currentResult) { - currentResult.setGroupLabel(this.renderGroupLabel(currentType, typeCounter, this.outline)); + currentResult.setGroupLabel(this.renderGroupLabel(currentType, typeCounter)); } } @@ -203,39 +220,14 @@ class OutlineModel extends QuickOpenModel { return elementARange.startLineNumber - elementBRange.startLineNumber; } - private renderGroupLabel(type: string, count: number, outline: Outline): string { - - const pattern = OutlineModel.getDefaultGroupLabelPatterns()[type]; + private renderGroupLabel(type: string, count: number): string { + const pattern = NLS_SYMBOL_CACHE[type]; if (pattern) { return strings.format(pattern, count); } return type; } - - private static getDefaultGroupLabelPatterns(): { [type: string]: string } { - const result: { [type: string]: string } = Object.create(null); - result['method'] = nls.localize('method', "methods ({0})"); - result['function'] = nls.localize('function', "functions ({0})"); - result['constructor'] = nls.localize('_constructor', "constructors ({0})"); - result['variable'] = nls.localize('variable', "variables ({0})"); - result['class'] = nls.localize('class', "classes ({0})"); - result['interface'] = nls.localize('interface', "interfaces ({0})"); - result['namespace'] = nls.localize('namespace', "namespaces ({0})"); - result['package'] = nls.localize('package', "packages ({0})"); - result['module'] = nls.localize('modules', "modules ({0})"); - result['property'] = nls.localize('property', "properties ({0})"); - result['enum'] = nls.localize('enum', "enumerations ({0})"); - result['string'] = nls.localize('string', "strings ({0})"); - result['rule'] = nls.localize('rule', "rules ({0})"); - result['file'] = nls.localize('file', "files ({0})"); - result['array'] = nls.localize('array', "arrays ({0})"); - result['number'] = nls.localize('number', "numbers ({0})"); - result['boolean'] = nls.localize('boolean', "booleans ({0})"); - result['object'] = nls.localize('object', "objects ({0})"); - result['key'] = nls.localize('key', "keys ({0})"); - return result; - } } class SymbolEntry extends EditorQuickOpenEntryGroup { @@ -246,9 +238,10 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { private icon: string; private description: string; private range: IRange; + private revealRange: IRange; private handler: GotoSymbolHandler; - constructor(index: number, name: string, type: string, description: string, icon: string, range: IRange, highlights: IHighlight[], editorService: IEditorService, handler: GotoSymbolHandler) { + constructor(index: number, name: string, type: string, description: string, icon: string, range: IRange, revealRange: IRange, highlights: IHighlight[], editorService: IEditorService, handler: GotoSymbolHandler) { super(); this.index = index; @@ -257,51 +250,52 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { this.icon = icon; this.description = description; this.range = range; + this.revealRange = revealRange || range; this.setHighlights(highlights); this.editorService = editorService; this.handler = handler; } - public getIndex(): number { + getIndex(): number { return this.index; } - public getLabel(): string { + getLabel(): string { return this.name; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, symbols", this.getLabel()); } - public getIcon(): string { + getIcon(): string { return this.icon; } - public getDescription(): string { + getDescription(): string { return this.description; } - public getType(): string { + getType(): string { return this.type; } - public getRange(): IRange { + getRange(): IRange { return this.range; } - public getInput(): IEditorInput { + getInput(): IEditorInput { return this.editorService.activeEditor; } - public getOptions(pinned?: boolean): ITextEditorOptions { + getOptions(pinned?: boolean): ITextEditorOptions { return { selection: this.toSelection(), pinned }; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { return this.runOpen(context); } @@ -349,18 +343,14 @@ class SymbolEntry extends EditorQuickOpenEntryGroup { private toSelection(): IRange { return { - startLineNumber: this.range.startLineNumber, - startColumn: this.range.startColumn || 1, - endLineNumber: this.range.startLineNumber, - endColumn: this.range.startColumn || 1 + startLineNumber: this.revealRange.startLineNumber, + startColumn: this.revealRange.startColumn || 1, + endLineNumber: this.revealRange.startLineNumber, + endColumn: this.revealRange.startColumn || 1 }; } } -interface Outline { - entries: SymbolInformation[]; -} - interface IEditorLineDecoration { groupId: GroupIdentifier; rangeHighlightId: string; @@ -369,24 +359,41 @@ interface IEditorLineDecoration { export class GotoSymbolHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.filesymbols'; + static readonly ID = 'workbench.picker.filesymbols'; - private outlineToModelCache: { [modelId: string]: OutlineModel; }; private rangeHighlightDecorationId: IEditorLineDecoration; private lastKnownEditorViewState: IEditorViewState; - private activeOutlineRequest: TPromise; + + private cachedOutlineRequest: TPromise; + private pendingOutlineRequest: CancellationTokenSource; constructor( @IEditorService private editorService: IEditorService ) { super(); - this.outlineToModelCache = {}; + this.registerListeners(); } - public getResults(searchValue: string): TPromise { + private registerListeners(): void { + this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()); + } + + private onDidActiveEditorChange(): void { + this.clearOutlineRequest(); + + this.lastKnownEditorViewState = void 0; + this.rangeHighlightDecorationId = void 0; + } + + getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); + // Support to cancel pending outline requests + if (!this.pendingOutlineRequest) { + this.pendingOutlineRequest = new CancellationTokenSource(); + } + // Remember view state to be able to restore on cancel if (!this.lastKnownEditorViewState) { const activeTextEditorWidget = this.editorService.activeTextEditorWidget; @@ -394,7 +401,10 @@ export class GotoSymbolHandler extends QuickOpenHandler { } // Resolve Outline Model - return this.getActiveOutline().then(outline => { + return this.getOutline().then(outline => { + if (token.isCancellationRequested) { + return outline; + } // Filter by search outline.applyFilter(searchValue); @@ -403,7 +413,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { }); } - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { if (searchString.length > 0) { return nls.localize('noSymbolsMatching', "No symbols matching"); } @@ -411,11 +421,11 @@ export class GotoSymbolHandler extends QuickOpenHandler { return nls.localize('noSymbolsFound', "No symbols found"); } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('gotoSymbolHandlerAriaLabel', "Type to narrow down symbols of the currently active editor."); } - public canRun(): boolean | string { + canRun(): boolean | string { let canRun = false; const activeTextEditorWidget = this.editorService.activeTextEditorWidget; @@ -433,7 +443,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { return canRun ? true : activeTextEditorWidget !== null ? nls.localize('cannotRunGotoSymbolInFile', "No symbol information for the file") : nls.localize('cannotRunGotoSymbol', "Open a text file first to go to a symbol"); } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { searchValue = searchValue.trim(); // Remove any type pattern (:) from search value as needed @@ -447,7 +457,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { }; } - private toQuickOpenEntries(flattened: SymbolInformation[]): SymbolEntry[] { + private toQuickOpenEntries(flattened: DocumentSymbol[]): SymbolEntry[] { const results: SymbolEntry[] = []; for (let i = 0; i < flattened.length; i++) { @@ -460,20 +470,20 @@ export class GotoSymbolHandler extends QuickOpenHandler { // Add results.push(new SymbolEntry(i, - label, icon, description, icon, - element.location.range, null, this.editorService, this + label, icon, description, `symbol-icon ${icon}`, + element.range, element.selectionRange, null, this.editorService, this )); } return results; } - private getActiveOutline(): TPromise { - if (!this.activeOutlineRequest) { - this.activeOutlineRequest = this.doGetActiveOutline(); + private getOutline(): TPromise { + if (!this.cachedOutlineRequest) { + this.cachedOutlineRequest = this.doGetActiveOutline(); } - return this.activeOutlineRequest; + return this.cachedOutlineRequest; } private doGetActiveOutline(): TPromise { @@ -485,29 +495,16 @@ export class GotoSymbolHandler extends QuickOpenHandler { } if (model && types.isFunction((model).getLanguageIdentifier)) { - - // Ask cache first - const modelId = (model).id; - if (this.outlineToModelCache[modelId]) { - return TPromise.as(this.outlineToModelCache[modelId]); - } - - return getDocumentSymbols(model).then(outline => { - - const model = new OutlineModel(outline, this.toQuickOpenEntries(outline.entries)); - - this.outlineToModelCache = {}; // Clear cache, only keep 1 outline - this.outlineToModelCache[modelId] = model; - - return model; - }); + return TPromise.wrap(asThenable(() => getDocumentSymbols(model, true, this.pendingOutlineRequest.token)).then(entries => { + return new OutlineModel(this.toQuickOpenEntries(entries)); + })); } } return TPromise.wrap(null); } - public decorateOutline(fullRange: IRange, startRange: IRange, editor: IEditor, group: IEditorGroup): void { + decorateOutline(fullRange: IRange, startRange: IRange, editor: IEditor, group: IEditorGroup): void { editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { const deleteDecorations: string[] = []; @@ -554,7 +551,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { }); } - public clearDecorations(): void { + private clearDecorations(): void { if (this.rangeHighlightDecorationId) { this.editorService.visibleControls.forEach(editor => { if (editor.group.id === this.rangeHighlightDecorationId.groupId) { @@ -572,10 +569,10 @@ export class GotoSymbolHandler extends QuickOpenHandler { } } - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { - // Clear Cache - this.outlineToModelCache = {}; + // Cancel any pending/cached outline request now + this.clearOutlineRequest(); // Clear Highlight Decorations if present this.clearDecorations(); @@ -586,9 +583,18 @@ export class GotoSymbolHandler extends QuickOpenHandler { if (activeTextEditorWidget) { activeTextEditorWidget.restoreViewState(this.lastKnownEditorViewState); } + + this.lastKnownEditorViewState = null; + } + } + + private clearOutlineRequest(): void { + if (this.pendingOutlineRequest) { + this.pendingOutlineRequest.cancel(); + this.pendingOutlineRequest.dispose(); + this.pendingOutlineRequest = void 0; } - this.lastKnownEditorViewState = null; - this.activeOutlineRequest = null; + this.cachedOutlineRequest = null; } } diff --git a/src/vs/workbench/parts/quickopen/browser/helpHandler.ts b/src/vs/workbench/parts/quickopen/browser/helpHandler.ts index 51881cef7dd..dbe3f74b4f7 100644 --- a/src/vs/workbench/parts/quickopen/browser/helpHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/helpHandler.ts @@ -10,8 +10,9 @@ import * as types from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { Mode, IEntryRunContext, IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenModel, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IQuickOpenRegistry, Extensions, QuickOpenHandler } from 'vs/workbench/browser/quickopen'; +import { IQuickOpenRegistry, Extensions, QuickOpenHandler, QuickOpenHandlerDescriptor, QuickOpenHandlerHelpEntry } from 'vs/workbench/browser/quickopen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const HELP_PREFIX = '?'; @@ -37,19 +38,19 @@ class HelpEntry extends QuickOpenEntryGroup { this.openOnPreview = openOnPreview; } - public getLabel(): string { + getLabel(): string { return this.prefixLabel; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, picker help", this.getLabel()); } - public getDescription(): string { + getDescription(): string { return this.description; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN || this.openOnPreview) { this.quickOpenService.show(this.prefix); } @@ -60,13 +61,13 @@ class HelpEntry extends QuickOpenEntryGroup { export class HelpHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.help'; + static readonly ID = 'workbench.picker.help'; constructor(@IQuickOpenService private quickOpenService: IQuickOpenService) { super(); } - public getResults(searchValue: string): TPromise { + getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); const registry = (Registry.as(Extensions.Quickopen)); @@ -79,9 +80,9 @@ export class HelpHandler extends QuickOpenHandler { const workbenchScoped: HelpEntry[] = []; const editorScoped: HelpEntry[] = []; - let entry: HelpEntry; - handlerDescriptors.sort((h1, h2) => h1.prefix.localeCompare(h2.prefix)).forEach((handlerDescriptor) => { + const matchingHandlers: (QuickOpenHandlerHelpEntry | QuickOpenHandlerDescriptor)[] = []; + handlerDescriptors.sort((h1, h2) => h1.prefix.localeCompare(h2.prefix)).forEach(handlerDescriptor => { if (handlerDescriptor.prefix !== HELP_PREFIX) { // Descriptor has multiple help entries @@ -90,19 +91,26 @@ export class HelpHandler extends QuickOpenHandler { const helpEntry = handlerDescriptor.helpEntries[j]; if (helpEntry.prefix.indexOf(searchValue) === 0) { - entry = new HelpEntry(helpEntry.prefix, helpEntry.description, this.quickOpenService, searchValue.length > 0); - if (helpEntry.needsEditor) { - editorScoped.push(entry); - } else { - workbenchScoped.push(entry); - } + matchingHandlers.push(helpEntry); } } } // Single Help entry for descriptor else if (handlerDescriptor.prefix.indexOf(searchValue) === 0) { - entry = new HelpEntry(handlerDescriptor.prefix, handlerDescriptor.description, this.quickOpenService, searchValue.length > 0); + matchingHandlers.push(handlerDescriptor); + } + } + }); + + matchingHandlers.forEach(handler => { + if (handler instanceof QuickOpenHandlerDescriptor) { + workbenchScoped.push(new HelpEntry(handler.prefix, handler.description, this.quickOpenService, matchingHandlers.length === 1)); + } else { + const entry = new HelpEntry(handler.prefix, handler.description, this.quickOpenService, matchingHandlers.length === 1); + if (handler.needsEditor) { + editorScoped.push(entry); + } else { workbenchScoped.push(entry); } } @@ -124,7 +132,7 @@ export class HelpHandler extends QuickOpenHandler { return TPromise.as(new QuickOpenModel([...workbenchScoped, ...editorScoped])); } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { searchValue = searchValue.trim(); return { autoFocusFirstEntry: searchValue.length > 0, diff --git a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg deleted file mode 100644 index ed2a1751005..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg deleted file mode 100644 index 173e427f964..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Constant_16x_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg deleted file mode 100755 index aa901ec1934..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg b/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg deleted file mode 100755 index 791759092fc..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/EnumItem_inverse_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg deleted file mode 100644 index 0e202ec10be..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg deleted file mode 100644 index a508edcd3d6..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Event_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg deleted file mode 100644 index ba2f2d091cf..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg deleted file mode 100644 index 21e1e814b2e..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Operator_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg deleted file mode 100644 index e776cbc5651..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg deleted file mode 100644 index 1b76b62be9a..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Structure_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg b/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg deleted file mode 100644 index 788cc8d6450..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg b/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg deleted file mode 100644 index 6cec71cb033..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/Template_16x_vscode_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css b/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css deleted file mode 100644 index 4d99facd57b..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/gotoSymbolHandler.css +++ /dev/null @@ -1,154 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant { - background-image: url('Constant_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constant { - background-image: url('Constant_16x_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member { - background-image: url('EnumItem_16x.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum-member { - background-image: url('EnumItem_inverse_16x.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct { - background-image: url('Structure_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.struct { - background-image: url('Structure_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event { - background-image: url('Event_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.event { - background-image: url('Event_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator { - background-image: url('Operator_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.operator { - background-image: url('Operator_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter { - background-image: url('Template_16x_vscode.svg'); - background-repeat: no-repeat; - background-position: 0 -2px; -} -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.type-parameter { - background-image: url('Template_16x_vscode_inverse.svg'); -} - -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { - background-image: url('symbol-sprite.svg'); - background-repeat: no-repeat; -} - -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor { background-position: 0 -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable { background-position: -22px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class { background-position: -43px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface { background-position: -63px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module { background-position: -82px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property { background-position: -102px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum { background-position: -122px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string { background-position: -202px -3px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule { background-position: -242px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file { background-position: -262px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array { background-position: -302px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number { background-position: -322px -4px; } -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { background-position: -343px -4px; } - -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.method, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.function, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.constructor { background-position: 0 -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.field, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.variable { background-position: -22px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.class { background-position: -43px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.interface { background-position: -63px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.object, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.namespace, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.package, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.module { background-position: -82px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.property { background-position: -102px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.key, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.string { background-position: -202px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.enum { background-position: -122px -23px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.rule { background-position: -242px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.file { background-position: -262px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.array { background-position: -302px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.number { background-position: -322px -24px; } -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.null, -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.boolean { background-position: -342px -24px; } diff --git a/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg b/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg deleted file mode 100644 index ee9a63dcf6f..00000000000 --- a/src/vs/workbench/parts/quickopen/browser/media/symbol-sprite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts b/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts index be4d6379082..638f3f36c58 100644 --- a/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts +++ b/src/vs/workbench/parts/quickopen/browser/quickopen.contribution.ts @@ -9,7 +9,7 @@ import * as env from 'vs/base/common/platform'; import * as nls from 'vs/nls'; import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions } from 'vs/workbench/browser/quickopen'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { GotoSymbolAction, GOTO_SYMBOL_PREFIX, SCOPE_PREFIX, GotoSymbolHandler } from 'vs/workbench/parts/quickopen/browser/gotoSymbolHandler'; @@ -19,7 +19,7 @@ import { HELP_PREFIX, HelpHandler } from 'vs/workbench/parts/quickopen/browser/h import { VIEW_PICKER_PREFIX, OpenViewPickerAction, QuickOpenViewPickerAction, ViewPickerHandler } from 'vs/workbench/parts/quickopen/browser/viewPickerHandler'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; // Register Actions const registry = Registry.as(ActionExtensions.WorkbenchActions); @@ -50,7 +50,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenViewPickerAct const quickOpenNavigateNextInViewPickerId = 'workbench.action.quickOpenNavigateNextInViewPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigateNextInViewPickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigateNextInViewPickerId, true), when: inViewsPickerContext, primary: viewPickerKeybinding.primary, @@ -61,7 +61,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const quickOpenNavigatePreviousInViewPickerId = 'workbench.action.quickOpenNavigatePreviousInViewPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickOpenNavigatePreviousInViewPickerId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(50), + weight: KeybindingWeight.WorkbenchContrib + 50, handler: getQuickNavigateHandler(quickOpenNavigatePreviousInViewPickerId, false), when: inViewsPickerContext, primary: viewPickerKeybinding.primary | KeyMod.Shift, @@ -144,4 +144,44 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen } ] ) -); \ No newline at end of file +); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: ShowAllCommandsAction.ID, + title: nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: OpenViewPickerAction.ID, + title: nls.localize({ key: 'miOpenView', comment: ['&& denotes a mnemonic'] }, "&&Open View...") + }, + order: 2 +}); + +// Go to menu + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.gotoSymbol', + title: nls.localize({ key: 'miGotoSymbolInFile', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in File...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.gotoLine', + title: nls.localize({ key: 'miGotoLine', comment: ['&& denotes a mnemonic'] }, "Go to &&Line...") + }, + order: 7 +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts index 711590c7b4a..f1e0a0a8ba2 100644 --- a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts @@ -6,7 +6,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenModel, QuickOpenEntryGroup, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { QuickOpenHandler, QuickOpenAction } from 'vs/workbench/browser/quickopen'; @@ -23,6 +22,7 @@ import { ViewsRegistry, ViewContainer, IViewsService, IViewContainersRegistry, E import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Registry } from 'vs/platform/registry/common/platform'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const VIEW_PICKER_PREFIX = 'view '; @@ -36,19 +36,19 @@ export class ViewEntry extends QuickOpenEntryGroup { super(); } - public getLabel(): string { + getLabel(): string { return this.label; } - public getCategory(): string { + getCategory(): string { return this.category; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, view picker", this.getLabel()); } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { if (mode === Mode.OPEN) { return this.runOpen(context); } @@ -67,7 +67,7 @@ export class ViewEntry extends QuickOpenEntryGroup { export class ViewPickerHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.views'; + static readonly ID = 'workbench.picker.views'; constructor( @IViewletService private viewletService: IViewletService, @@ -80,7 +80,7 @@ export class ViewPickerHandler extends QuickOpenHandler { super(); } - public getResults(searchValue: string): TPromise { + getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); const normalizedSearchValueLowercase = stripWildcards(searchValue).toLowerCase(); @@ -137,11 +137,11 @@ export class ViewPickerHandler extends QuickOpenHandler { // Viewlets const viewlets = this.viewletService.getViewlets(); - viewlets.forEach((viewlet, index) => viewEntries.push(new ViewEntry(viewlet.name, nls.localize('views', "Views"), () => this.viewletService.openViewlet(viewlet.id, true).done(null, errors.onUnexpectedError)))); + viewlets.forEach((viewlet, index) => viewEntries.push(new ViewEntry(viewlet.name, nls.localize('views', "Views"), () => this.viewletService.openViewlet(viewlet.id, true)))); // Panels const panels = this.panelService.getPanels(); - panels.forEach((panel, index) => viewEntries.push(new ViewEntry(panel.name, nls.localize('panels', "Panels"), () => this.panelService.openPanel(panel.id, true).done(null, errors.onUnexpectedError)))); + panels.forEach((panel, index) => viewEntries.push(new ViewEntry(panel.name, nls.localize('panels', "Panels"), () => this.panelService.openPanel(panel.id, true)))); // Viewlet Views viewlets.forEach((viewlet, index) => { @@ -158,9 +158,9 @@ export class ViewPickerHandler extends QuickOpenHandler { tab.terminalInstances.forEach((terminal, terminalIndex) => { const index = `${tabIndex + 1}.${terminalIndex + 1}`; const entry = new ViewEntry(nls.localize('terminalTitle', "{0}: {1}", index, terminal.title), terminalsCategory, () => { - this.terminalService.showPanel(true).done(() => { + this.terminalService.showPanel(true).then(() => { this.terminalService.setActiveInstance(terminal); - }, errors.onUnexpectedError); + }); }); viewEntries.push(entry); @@ -168,10 +168,10 @@ export class ViewPickerHandler extends QuickOpenHandler { }); // Output Channels - const channels = this.outputService.getChannels(); + const channels = this.outputService.getChannelDescriptors(); channels.forEach((channel, index) => { const outputCategory = nls.localize('channels', "Output"); - const entry = new ViewEntry(channel.label, outputCategory, () => this.outputService.showChannel(channel.id).done(null, errors.onUnexpectedError)); + const entry = new ViewEntry(channel.label, outputCategory, () => this.outputService.showChannel(channel.id)); viewEntries.push(entry); }); @@ -179,7 +179,7 @@ export class ViewPickerHandler extends QuickOpenHandler { return viewEntries; } - public getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { + getAutoFocus(searchValue: string, context: { model: IModel, quickNavigateConfiguration?: IQuickNavigateConfiguration }): IAutoFocus { return { autoFocusFirstEntry: !!searchValue || !!context.quickNavigateConfiguration }; @@ -188,8 +188,8 @@ export class ViewPickerHandler extends QuickOpenHandler { export class OpenViewPickerAction extends QuickOpenAction { - public static readonly ID = 'workbench.action.openView'; - public static readonly LABEL = nls.localize('openView', "Open View"); + static readonly ID = 'workbench.action.openView'; + static readonly LABEL = nls.localize('openView', "Open View"); constructor( id: string, @@ -202,8 +202,8 @@ export class OpenViewPickerAction extends QuickOpenAction { export class QuickOpenViewPickerAction extends Action { - public static readonly ID = 'workbench.action.quickOpenView'; - public static readonly LABEL = nls.localize('quickOpenView', "Quick Open View"); + static readonly ID = 'workbench.action.quickOpenView'; + static readonly LABEL = nls.localize('quickOpenView', "Quick Open View"); constructor( id: string, @@ -214,7 +214,7 @@ export class QuickOpenViewPickerAction extends Action { super(id, label); } - public run(): TPromise { + run(): TPromise { const keys = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(VIEW_PICKER_PREFIX, { quickNavigateConfiguration: { keybindings: keys } }); 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 9981d15cb98..31f03d38792 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } 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 { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { RunOnceScheduler } from 'vs/base/common/async'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -30,9 +30,7 @@ interface IConfiguration extends IWindowsConfiguration { files: { useExperimentalFileWatcher: boolean, watcherExclude: object }; } -export class SettingsChangeRelauncher implements IWorkbenchContribution { - - private toDispose: IDisposable[] = []; +export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { private titleBarStyle: 'native' | 'custom'; private nativeTabs: boolean; @@ -59,6 +57,8 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IExtensionService private extensionService: IExtensionService ) { + super(); + const workspace = this.contextService.getWorkspace(); this.firstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : void 0; this.extensionHostRestarter = new RunOnceScheduler(() => this.extensionService.restartExtensionHost(), 10); @@ -70,15 +70,15 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { } private registerListeners(): void { - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(this.configurationService.getValue(), true))); - this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => setTimeout(() => this.handleWorkbenchState()))); + 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 { let changed = false; - // macOS: Titlebar style - if (isMacintosh && config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { + // Titlebar style + if (config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { this.titleBarStyle = config.window.titleBarStyle; changed = true; } @@ -201,10 +201,6 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { return void 0; }); } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts index 973f975cb0e..e54d6529b45 100644 --- a/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts @@ -8,8 +8,8 @@ import * as nls from 'vs/nls'; import 'vs/css!./media/dirtydiffDecorator'; -import { ThrottledDelayer, always } from 'vs/base/common/async'; -import { IDisposable, dispose, toDisposable, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ThrottledDelayer, always, first } from 'vs/base/common/async'; +import { IDisposable, dispose, toDisposable, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter, anyEvent as anyEvent, filterEvent, once } from 'vs/base/common/event'; import * as ext from 'vs/workbench/common/contributions'; @@ -19,7 +19,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ISCMService, ISCMRepository } from 'vs/workbench/services/scm/common/scm'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { registerThemingParticipant, ITheme, ICssStyleCollector, themeColorFromId, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,7 +33,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Position } from 'vs/editor/common/core/position'; import { rot } from 'vs/base/common/numbers'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/referenceSearch/referencesWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -64,7 +64,7 @@ class DiffMenuItemActionItem extends MenuItemActionItem { event.stopPropagation(); this.actionRunner.run(this._commandAction, this._context) - .done(undefined, err => this._notificationService.error(err)); + .then(undefined, err => this._notificationService.error(err)); } } @@ -372,7 +372,7 @@ export class ShowPreviousChangeAction extends EditorAction { label: nls.localize('show previous change', "Show Previous Change"), alias: 'Show Previous Change', precondition: null, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3 } + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -406,7 +406,7 @@ export class ShowNextChangeAction extends EditorAction { label: nls.localize('show next change', "Show Next Change"), alias: 'Show Next Change', precondition: null, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3 } + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -440,7 +440,7 @@ export class MoveToPreviousChangeAction extends EditorAction { label: nls.localize('move to previous change', "Move to Previous Change"), alias: 'Move to Previous Change', precondition: null, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5 } + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -482,7 +482,7 @@ export class MoveToNextChangeAction extends EditorAction { label: nls.localize('move to next change', "Move to Next Change"), alias: 'Move to Next Change', precondition: null, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5 } + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -518,7 +518,7 @@ registerEditorAction(MoveToNextChangeAction); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeDirtyDiff', - weight: KeybindingsRegistry.WEIGHT.editorContrib(50), + weight: KeybindingWeight.EditorContrib + 50, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(isDirtyDiffVisible), @@ -553,7 +553,7 @@ export class DirtyDiffController implements IEditorContribution { private widget: DirtyDiffWidget | null = null; private currentIndex: number = -1; private readonly isDirtyDiffVisible: IContextKey; - private session: IDisposable = EmptyDisposable; + private session: IDisposable = Disposable.None; private mouseDownInfo: { lineNumber: number } | null = null; private enabled = false; private disposables: IDisposable[] = []; @@ -611,7 +611,7 @@ export class DirtyDiffController implements IEditorContribution { close(): void { this.session.dispose(); - this.session = EmptyDisposable; + this.session = Disposable.None; } private assertWidget(): boolean { @@ -703,11 +703,16 @@ export class DirtyDiffController implements IEditorContribution { return; } + if (e.target.element.className.indexOf('dirty-diff-glyph') < 0) { + return; + } + const data = e.target.detail as IMarginData; - const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; + const offsetLeftInGutter = (e.target.element as HTMLElement).offsetLeft; + const gutterOffsetX = data.offsetX - offsetLeftInGutter; // TODO@joao TODO@alex TODO@martin this is such that we don't collide with folding - if (gutterOffsetX > 10) { + if (gutterOffsetX < -3 || gutterOffsetX > 6) { // dirty diff decoration on hover is 9px wide return; } @@ -1032,20 +1037,13 @@ export class DirtyDiffModel { }); } - private async getOriginalResource(): TPromise { + private getOriginalResource(): TPromise { if (!this._editorModel) { - return null; + return TPromise.as(null); } - for (const repository of this.scmService.repositories) { - const result = repository.provider.getOriginalResource(this._editorModel.uri); - - if (result) { - return result; - } - } - - return null; + const uri = this._editorModel.uri; + return first(this.scmService.repositories.map(r => () => r.provider.getOriginalResource(uri))); } findNextClosestChange(lineNumber: number, inclusive = true): number { diff --git a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css index e55cb63c612..8a884647c66 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css @@ -6,6 +6,7 @@ .monaco-editor .dirty-diff-glyph { margin-left: 5px; cursor: pointer; + z-index: 5; } .monaco-editor .dirty-diff-deleted:after { @@ -19,6 +20,7 @@ border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear; + pointer-events: none; } .monaco-editor .dirty-diff-glyph:before { 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 f6e3ec0c18a..108bf404719 100644 --- a/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -15,7 +15,6 @@ .scm-viewlet .empty-message { padding: 10px 22px 0 22px; - opacity: 0.5; } .scm-viewlet:not(.empty) .empty-message, @@ -51,6 +50,7 @@ display: none; } +.scm-viewlet .scm-provider > .type, .scm-viewlet .scm-provider > .name > .type { opacity: 0.7; margin-left: 0.5em; diff --git a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts index 56943a892a0..453a5aa6da3 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts @@ -13,7 +13,7 @@ import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, To import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusUpdater, StatusBarController } from './scmActivity'; import { SCMViewlet } from 'vs/workbench/parts/scm/electron-browser/scmViewlet'; @@ -71,7 +71,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis properties: { 'scm.alwaysShowProviders': { type: 'boolean', - description: localize('alwaysShowProviders', "Whether to always show the Source Control Provider section."), + description: localize('alwaysShowProviders', "Controls whether to always show the Source Control Provider section."), default: false }, 'scm.diffDecorations': { @@ -88,3 +88,14 @@ Registry.as(ConfigurationExtensions.Configuration).regis } } }); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEWLET_ID, + title: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM") + }, + order: 3 +}); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts index 9464225a74d..c0c4f11d15e 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/paths'; -import { IDisposable, dispose, empty as EmptyDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { filterEvent, anyEvent as anyEvent } from 'vs/base/common/event'; import { VIEWLET_ID } from 'vs/workbench/parts/scm/common/scm'; import { ISCMService, ISCMRepository } from 'vs/workbench/services/scm/common/scm'; @@ -18,7 +18,7 @@ import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } export class StatusUpdater implements IWorkbenchContribution { - private badgeDisposable: IDisposable = EmptyDisposable; + private badgeDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; constructor( @@ -60,7 +60,7 @@ export class StatusUpdater implements IWorkbenchContribution { const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); this.badgeDisposable = this.activityService.showActivity(VIEWLET_ID, badge, 'scm-viewlet-label'); } else { - this.badgeDisposable = EmptyDisposable; + this.badgeDisposable = Disposable.None; } } @@ -72,8 +72,8 @@ export class StatusUpdater implements IWorkbenchContribution { export class StatusBarController implements IWorkbenchContribution { - private statusBarDisposable: IDisposable = EmptyDisposable; - private focusDisposable: IDisposable = EmptyDisposable; + private statusBarDisposable: IDisposable = Disposable.None; + private focusDisposable: IDisposable = Disposable.None; private focusedRepository: ISCMRepository | undefined = undefined; private focusedProviderContextKey: IContextKey; private disposables: IDisposable[] = []; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 4bc49cd2892..0c9f3c9b0b6 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -11,13 +11,12 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter, chain, mapEvent, anyEvent, filterEvent, latch } from 'vs/base/common/event'; import { domEvent, stop } from 'vs/base/browser/event'; import { basename } from 'vs/base/common/paths'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { PanelViewlet, ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, Dimension, addDisposableListener } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IVirtualDelegate, IRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/parts/scm/common/scm'; import { FileLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -39,8 +38,6 @@ import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/commo import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/parts/extensions/common/extensions'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -57,7 +54,7 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IViewDescriptorRef, PersistentContributableViewsModel, IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; -import { IViewDescriptor, IViewsViewlet } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewsViewlet, IView } from 'vs/workbench/common/views'; import { IPanelDndController, Panel } from 'vs/base/browser/ui/splitview/panelview'; export interface ISpliceEvent { @@ -77,7 +74,7 @@ export interface IViewModel { hide(repository: ISCMRepository): void; } -class ProvidersListDelegate implements IDelegate { +class ProvidersListDelegate implements IVirtualDelegate { getHeight(element: ISCMRepository): number { return 22; @@ -109,9 +106,9 @@ class StatusBarActionItem extends ActionItem { super(null, action, {}); } - _updateLabel(): void { + updateLabel(): void { if (this.options.label) { - this.$e.innerHtml(renderOcticons(this.getAction().label)); + this.label.innerHTML = renderOcticons(this.getAction().label); } } } @@ -144,7 +141,7 @@ class ProviderRenderer implements IRenderer new StatusBarActionItem(a as StatusBarAction) }); - const disposable = EmptyDisposable; + const disposable = Disposable.None; const templateDisposable = combinedDisposable([actionBar, badgeStyler]); return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; @@ -192,6 +189,10 @@ class ProviderRenderer implements IRenderer { @@ -439,6 +440,10 @@ class ResourceGroupRenderer implements IRenderer { const decorationIcon = append(element, $('.decoration-icon')); return { - element, name, fileLabel, decorationIcon, actionBar, elementDisposable: EmptyDisposable, dispose: () => { + element, name, fileLabel, decorationIcon, actionBar, elementDisposable: Disposable.None, dispose: () => { actionBar.dispose(); fileLabel.dispose(); } @@ -559,13 +564,17 @@ class ResourceRenderer implements IRenderer { template.elementDisposable = combinedDisposable(disposables); } + disposeElement(): void { + // noop + } + disposeTemplate(template: ResourceTemplate): void { template.elementDisposable.dispose(); template.dispose(); } } -class ProviderListDelegate implements IDelegate { +class ProviderListDelegate implements IVirtualDelegate { getHeight() { return 22; } @@ -740,10 +749,6 @@ export class RepositoryPanel extends ViewletPanel { private menus: SCMMenus; private visibilityDisposables: IDisposable[] = []; - get onDidChangeTitle(): Event { - return this.menus.onDidChangeTitle; - } - constructor( id: string, readonly repository: ISCMRepository, @@ -762,6 +767,7 @@ export class RepositoryPanel extends ViewletPanel { ) { super({ id, title: repository.provider.label }, keybindingService, contextMenuService, configurationService); this.menus = instantiationService.createInstance(SCMMenus, repository.provider); + this.menus.onDidChangeTitle(this._onDidChangeTitleArea.fire, this._onDidChangeTitleArea, this.disposables); } render(): void { @@ -770,19 +776,20 @@ export class RepositoryPanel extends ViewletPanel { } protected renderHeaderTitle(container: HTMLElement): void { - const header = append(container, $('.title.scm-provider')); - const name = append(header, $('.name')); - const title = append(name, $('span.title')); - const type = append(name, $('span.type')); + let title: string; + let type: string; if (this.repository.provider.rootUri) { - title.textContent = basename(this.repository.provider.rootUri.fsPath); - type.textContent = this.repository.provider.label; + title = basename(this.repository.provider.rootUri.fsPath); + type = this.repository.provider.label; } else { - title.textContent = this.repository.provider.label; - type.textContent = ''; + title = this.repository.provider.label; + type = ''; } + super.renderHeaderTitle(container, title); + addClass(container, 'scm-provider'); + append(container, $('span.type', null, type)); const onContextMenu = mapEvent(stop(domEvent(container, 'contextmenu')), e => new StandardMouseEvent(e)); onContextMenu(this.onContextMenu, this, this.disposables); } @@ -816,12 +823,9 @@ export class RepositoryPanel extends ViewletPanel { this.inputBox.setPlaceHolder(placeholder); }; - const validationDelayer = new ThrottledDelayer(200); - + const validationDelayer = new ThrottledDelayer(200); const validate = () => { - validationDelayer.trigger(async (): TPromise => { - const result = await this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart); - + return this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart).then(result => { if (!result) { this.inputBox.inputElement.removeAttribute('aria-invalid'); this.inputBox.hideMessage(); @@ -832,15 +836,17 @@ export class RepositoryPanel extends ViewletPanel { }); }; + const triggerValidation = () => validationDelayer.trigger(validate); + this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true }); this.disposables.push(attachInputBoxStyler(this.inputBox, this.themeService)); this.disposables.push(this.inputBox); - this.inputBox.onDidChange(validate, null, this.disposables); + this.inputBox.onDidChange(triggerValidation, null, this.disposables); const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup'); const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup'); - anyEvent(onKeyUp, onMouseUp)(() => validate(), null, this.disposables); + anyEvent(onKeyUp, onMouseUp)(triggerValidation, null, this.disposables); this.inputBox.value = this.repository.input.value; this.inputBox.onDidChange(value => this.repository.input.value = value, null, this.disposables); @@ -949,7 +955,7 @@ export class RepositoryPanel extends ViewletPanel { } private open(e: ISCMResource): void { - e.open().done(undefined, onUnexpectedError); + e.open(); } private pin(): void { @@ -998,8 +1004,7 @@ export class RepositoryPanel extends ViewletPanel { const id = this.repository.provider.acceptInputCommand.id; const args = this.repository.provider.acceptInputCommand.arguments; - this.commandService.executeCommand(id, ...args) - .done(undefined, onUnexpectedError); + this.commandService.executeCommand(id, ...args); } dispose(): void { @@ -1008,21 +1013,6 @@ export class RepositoryPanel extends ViewletPanel { } } -class InstallAdditionalSCMProvidersAction extends Action { - - constructor(@IViewletService private viewletService: IViewletService) { - super('scm.installAdditionalSCMProviders', localize('installAdditionalSCMProviders', "Install Additional SCM Providers..."), '', true); - } - - run(): TPromise { - return this.viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true).then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search('category:"SCM Providers" @sort:installs'); - viewlet.focus(); - }); - } -} - class SCMPanelDndController implements IPanelDndController { canDrag(panel: Panel): boolean { @@ -1040,10 +1030,10 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle private menus: SCMMenus; private mainPanel: MainPanel | null = null; private cachedMainPanelHeight: number | undefined; - private mainPanelDisposable: IDisposable = EmptyDisposable; + private mainPanelDisposable: IDisposable = Disposable.None; private _repositories: ISCMRepository[] = []; private repositoryPanels: RepositoryPanel[] = []; - private singleRepositoryPanelTitleActionsDisposable: IDisposable = EmptyDisposable; + private singlePanelTitleActionsDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; private lastFocusedRepository: ISCMRepository | undefined; @@ -1088,37 +1078,37 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle this.disposables.push(this.contributedViews); } - async create(parent: HTMLElement): TPromise { - await super.create(parent); + create(parent: HTMLElement): TPromise { + return super.create(parent).then(() => { + this.el = parent; + addClass(this.el, 'scm-viewlet'); + addClass(this.el, 'empty'); + append(parent, $('div.empty-message', null, localize('no open repo', "No source control providers registered."))); - this.el = parent; - addClass(this.el, 'scm-viewlet'); - addClass(this.el, 'empty'); - append(parent, $('div.empty-message', null, localize('no open repo', "There are no active source control providers."))); + this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); + const onDidUpdateConfiguration = filterEvent(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowProviders')); + onDidUpdateConfiguration(this.onDidChangeRepositories, this, this.disposables); - const onDidUpdateConfiguration = filterEvent(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowProviders')); - onDidUpdateConfiguration(this.onDidChangeRepositories, this, this.disposables); + this.onDidChangeRepositories(); - this.onDidChangeRepositories(); + this.contributedViews.onDidAdd(this.onDidAddContributedViews, this, this.disposables); + this.contributedViews.onDidRemove(this.onDidRemoveContributedViews, this, this.disposables); - this.contributedViews.onDidAdd(this.onDidAddContributedViews, this, this.disposables); - this.contributedViews.onDidRemove(this.onDidRemoveContributedViews, this, this.disposables); + let index = this.getContributedViewsStartIndex(); + const contributedViews: IAddedViewDescriptorRef[] = this.contributedViews.visibleViewDescriptors.map(viewDescriptor => { + const size = this.contributedViews.getSize(viewDescriptor.id); + const collapsed = this.contributedViews.isCollapsed(viewDescriptor.id); + return { viewDescriptor, index: index++, size, collapsed }; + }); + if (contributedViews.length) { + this.onDidAddContributedViews(contributedViews); + } - let index = this.getContributedViewsStartIndex(); - const contributedViews: IAddedViewDescriptorRef[] = this.contributedViews.visibleViewDescriptors.map(viewDescriptor => { - const size = this.contributedViews.getSize(viewDescriptor.id); - const collapsed = this.contributedViews.isCollapsed(viewDescriptor.id); - return { viewDescriptor, index: index++, size, collapsed }; + this.onDidSashChange(this.saveContributedViewSizes, this, this.disposables); }); - if (contributedViews.length) { - this.onDidAddContributedViews(contributedViews); - } - - this.onDidSashChange(this.saveContributedViewSizes, this, this.disposables); } private onDidAddRepository(repository: ISCMRepository): void { @@ -1173,7 +1163,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle }); } else { this.mainPanelDisposable.dispose(); - this.mainPanelDisposable = EmptyDisposable; + this.mainPanelDisposable = Disposable.None; this.mainPanel = null; } } @@ -1226,26 +1216,11 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle } getSecondaryActions(): IAction[] { - let result: IAction[]; - if (this.isSingleView()) { - const [panel] = this.panels; - - result = [ - ...panel.getSecondaryActions(), - new Separator() - ]; + return this.panels[0].getSecondaryActions(); } else { - result = [...this.menus.getTitleSecondaryActions()]; - - if (result.length > 0) { - result.push(new Separator()); - } + return this.menus.getTitleSecondaryActions(); } - - result.push(this.instantiationService.createInstance(InstallAdditionalSCMProvidersAction)); - - return result; } getActionItem(action: IAction): IActionItem { @@ -1308,7 +1283,8 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle this.addPanels([{ panel, size: panel.minimumSize, index: index++ }]); panel.repository.focus(); panel.onDidFocus(() => this.lastFocusedRepository = panel.repository); - if (newRepositoryPanels.length === 1 || this.lastFocusedRepository === panel.repository) { + + if (this.lastFocusedRepository === panel.repository) { panel.focus(); } }); @@ -1335,14 +1311,19 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle // React to menu changes for single view mode if (wasSingleView !== this.isSingleView()) { - this.singleRepositoryPanelTitleActionsDisposable.dispose(); + this.singlePanelTitleActionsDisposable.dispose(); if (this.isSingleView()) { - this.singleRepositoryPanelTitleActionsDisposable = this.repositoryPanels[0].onDidChangeTitle(this.updateTitleArea, this); + this.singlePanelTitleActionsDisposable = this.panels[0].onDidChangeTitleArea(this.updateTitleArea, this); } this.updateTitleArea(); } + + if (this.isVisible()) { + panelsToRemove.forEach(p => p.repository.setSelected(false)); + newRepositoryPanels.forEach(p => p.repository.setSelected(true)); + } } private getContributableViewsSize(): number { @@ -1473,14 +1454,18 @@ export class SCMViewlet extends PanelViewlet implements IViewModel, IViewsViewle return super.isSingleView() && this.repositoryPanels.length + this.contributedViews.visibleViewDescriptors.length === 1; } - openView(id: string, focus?: boolean): TPromise { - this.contributedViews.setVisible(id, true); - const panel = this.panels.filter(panel => panel instanceof ViewletPanel && panel.id === id)[0]; - if (panel) { - panel.setExpanded(true); - panel.focus(); + openView(id: string, focus?: boolean): TPromise { + if (focus) { + this.focus(); } - return TPromise.as(null); + let panel = this.panels.filter(panel => panel instanceof ViewletPanel && panel.id === id)[0]; + if (!panel) { + this.contributedViews.setVisible(id, true); + } + panel = this.panels.filter(panel => panel instanceof ViewletPanel && panel.id === id)[0]; + panel.setExpanded(true); + panel.focus(); + return TPromise.as(panel); } hide(repository: ISCMRepository): void { diff --git a/src/vs/workbench/parts/search/browser/media/stop-inverse.svg b/src/vs/workbench/parts/search/browser/media/stop-inverse.svg index ef79528e9c8..3740e50c8c0 100644 --- a/src/vs/workbench/parts/search/browser/media/stop-inverse.svg +++ b/src/vs/workbench/parts/search/browser/media/stop-inverse.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/vs/workbench/parts/search/browser/media/stop.svg b/src/vs/workbench/parts/search/browser/media/stop.svg index 0b36e84ac92..9a036f2e620 100644 --- a/src/vs/workbench/parts/search/browser/media/stop.svg +++ b/src/vs/workbench/parts/search/browser/media/stop.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts index 3f0669240a6..2a9b5e67ae4 100644 --- a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts +++ b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts @@ -20,10 +20,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkbenchSearchConfiguration } from 'vs/workbench/parts/search/common/search'; import { IRange } from 'vs/editor/common/core/range'; import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; - -export import OpenSymbolHandler = openSymbolHandler.OpenSymbolHandler; // OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load import { INotificationService } from 'vs/platform/notification/common/notification'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export import OpenSymbolHandler = openSymbolHandler.OpenSymbolHandler; // OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load interface ISearchWithRange { search: string; @@ -32,19 +33,17 @@ interface ISearchWithRange { export class OpenAnythingHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.anything'; + static readonly ID = 'workbench.picker.anything'; private static readonly LINE_COLON_PATTERN = /[#|:|\(](\d*)([#|:|,](\d*))?\)?$/; - private static readonly FILE_SEARCH_DELAY = 300; - private static readonly SYMBOL_SEARCH_DELAY = 500; // go easier on those symbols! + private static readonly TYPING_SEARCH_DELAY = 200; // This delay accommodates for the user typing a word and then stops typing to start searching private static readonly MAX_DISPLAYED_RESULTS = 512; private openSymbolHandler: OpenSymbolHandler; private openFileHandler: OpenFileHandler; private searchDelayer: ThrottledDelayer; - private pendingSearch: TPromise; private isClosed: boolean; private scorerCache: ScorerCache; private includeSymbols: boolean; @@ -57,7 +56,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { super(); this.scorerCache = Object.create(null); - this.searchDelayer = new ThrottledDelayer(OpenAnythingHandler.FILE_SEARCH_DELAY); + this.searchDelayer = new ThrottledDelayer(OpenAnythingHandler.TYPING_SEARCH_DELAY); this.openSymbolHandler = instantiationService.createInstance(OpenSymbolHandler); this.openFileHandler = instantiationService.createInstance(OpenFileHandler); @@ -87,8 +86,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { }); } - public getResults(searchValue: string): TPromise { - this.cancelPendingSearch(); + getResults(searchValue: string, token: CancellationToken): TPromise { this.isClosed = false; // Treat this call as the handler being in use // Find a suitable range from the pattern looking for ":" and "#" @@ -104,24 +102,23 @@ export class OpenAnythingHandler extends QuickOpenHandler { } // The throttler needs a factory for its promises - const promiseFactory = () => { + const resultsPromise = () => { const resultPromises: TPromise[] = []; // File Results - const filePromise = this.openFileHandler.getResults(query.value, OpenAnythingHandler.MAX_DISPLAYED_RESULTS); + const filePromise = this.openFileHandler.getResults(query.original, token, OpenAnythingHandler.MAX_DISPLAYED_RESULTS); resultPromises.push(filePromise); // Symbol Results (unless disabled or a range or absolute path is specified) if (this.includeSymbols && !searchWithRange) { - resultPromises.push(this.openSymbolHandler.getResults(query.value)); + resultPromises.push(this.openSymbolHandler.getResults(query.original, token)); } // Join and sort unified - this.pendingSearch = TPromise.join(resultPromises).then(results => { - this.pendingSearch = null; + return TPromise.join(resultPromises).then(results => { // If the quick open widget has been closed meanwhile, ignore the result - if (this.isClosed) { + if (this.isClosed || token.isCancellationRequested) { return TPromise.as(new QuickOpenModel()); } @@ -144,8 +141,6 @@ export class OpenAnythingHandler extends QuickOpenHandler { return TPromise.as(new QuickOpenModel(viewResults)); }, error => { - this.pendingSearch = null; - if (!isPromiseCanceledError(error)) { if (error && error[0] && error[0].message) { this.notificationService.error(error[0].message.replace(/[\*_\[\]]/g, '\\$&')); @@ -156,15 +151,13 @@ export class OpenAnythingHandler extends QuickOpenHandler { return null; }); - - return this.pendingSearch; }; // Trigger through delayer to prevent accumulation while the user is typing (except when expecting results to come from cache) - return this.hasShortResponseTime() ? promiseFactory() : this.searchDelayer.trigger(promiseFactory, this.includeSymbols ? OpenAnythingHandler.SYMBOL_SEARCH_DELAY : OpenAnythingHandler.FILE_SEARCH_DELAY); + return this.hasShortResponseTime() ? resultsPromise() : this.searchDelayer.trigger(resultsPromise, OpenAnythingHandler.TYPING_SEARCH_DELAY); } - public hasShortResponseTime(): boolean { + hasShortResponseTime(): boolean { if (!this.includeSymbols) { return this.openFileHandler.hasShortResponseTime(); } @@ -228,27 +221,24 @@ export class OpenAnythingHandler extends QuickOpenHandler { return null; } - public getGroupLabel(): string { + getGroupLabel(): string { return this.includeSymbols ? nls.localize('fileAndTypeResults', "file and symbol results") : nls.localize('fileResults', "file results"); } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { return { autoFocusFirstEntry: true }; } - public onOpen(): void { + onOpen(): void { this.openSymbolHandler.onOpen(); this.openFileHandler.onOpen(); } - public onClose(canceled: boolean): void { + onClose(canceled: boolean): void { this.isClosed = true; - // Cancel any pending search - this.cancelPendingSearch(); - // Clear Cache this.scorerCache = Object.create(null); @@ -256,11 +246,4 @@ export class OpenAnythingHandler extends QuickOpenHandler { this.openSymbolHandler.onClose(canceled); this.openFileHandler.onClose(canceled); } - - private cancelPendingSearch(): void { - if (this.pendingSearch) { - this.pendingSearch.cancel(); - this.pendingSearch = null; - } - } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/search/browser/openFileHandler.ts b/src/vs/workbench/parts/search/browser/openFileHandler.ts index 2a08068c698..27043a280bc 100644 --- a/src/vs/workbench/parts/search/browser/openFileHandler.ts +++ b/src/vs/workbench/parts/search/browser/openFileHandler.ts @@ -8,10 +8,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; -import * as labels from 'vs/base/common/labels'; import * as objects from 'vs/base/common/objects'; import { defaultGenerator } from 'vs/base/common/idGenerator'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -26,16 +25,21 @@ import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQueryOptions, ISearchService, ISearchStats, ISearchQuery } from 'vs/platform/search/common/search'; +import { IQueryOptions, ISearchService, IFileSearchStats, ISearchQuery, ISearchComplete } from 'vs/platform/search/common/search'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IRange } from 'vs/editor/common/core/range'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/parts/search/common/search'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { prepareQuery, IPreparedQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { untildify } from 'vs/base/common/labels'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class FileQuickOpenModel extends QuickOpenModel { - constructor(entries: QuickOpenEntry[], public stats?: ISearchStats) { + constructor(entries: QuickOpenEntry[], stats?: IFileSearchStats) { super(entries); } } @@ -57,41 +61,41 @@ export class FileEntry extends EditorQuickOpenEntry { super(editorService); } - public getLabel(): string { + getLabel(): string { return this.name; } - public getLabelOptions(): IIconLabelValueOptions { + getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.resource) }; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, file picker", this.getLabel()); } - public getDescription(): string { + getDescription(): string { return this.description; } - public getIcon(): string { + getIcon(): string { return this.icon; } - public getResource(): URI { + getResource(): URI { return this.resource; } - public setRange(range: IRange): void { + setRange(range: IRange): void { this.range = range; } - public mergeWithEditorHistory(): boolean { + mergeWithEditorHistory(): boolean { return true; } - public getInput(): IResourceInput | EditorInput { + getInput(): IResourceInput | EditorInput { const input: IResourceInput = { resource: this.resource, options: { @@ -122,70 +126,101 @@ export class OpenFileHandler extends QuickOpenHandler { @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @ISearchService private searchService: ISearchService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IFileService private fileService: IFileService, + @ILabelService private labelService: ILabelService ) { super(); this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } - public setOptions(options: IOpenFileOptions) { + setOptions(options: IOpenFileOptions) { this.options = options; } - public getResults(searchValue: string, maxSortedResults?: number): TPromise { - searchValue = searchValue.trim(); + getResults(searchValue: string, token: CancellationToken, maxSortedResults?: number): TPromise { + const query = prepareQuery(searchValue); // Respond directly to empty search - if (!searchValue) { + if (!query.value) { return TPromise.as(new FileQuickOpenModel([])); } // Untildify file pattern - searchValue = labels.untildify(searchValue, this.environmentService.userHome); + query.value = untildify(query.value, this.environmentService.userHome); // Do find results - return this.doFindResults(searchValue, this.cacheState.cacheKey, maxSortedResults); + return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults); } - private doFindResults(searchValue: string, cacheKey?: string, maxSortedResults?: number): TPromise { - const query: IQueryOptions = { - extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService), - filePattern: searchValue, - cacheKey: cacheKey - }; - - if (typeof maxSortedResults === 'number') { - query.maxResults = maxSortedResults; - query.sortByScore = true; - } + private doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): TPromise { + const queryOptions = this.doResolveQueryOptions(query, cacheKey, maxSortedResults); let iconClass: string; if (this.options && this.options.forceUseIcons && !this.themeService.getFileIconTheme()) { iconClass = 'file'; // only use a generic file icon if we are forced to use an icon and have no icon theme set otherwise } - const folderResources = this.contextService.getWorkspace().folders.map(folder => folder.uri); - return this.searchService.search(this.queryBuilder.file(folderResources, query)).then((complete) => { - const results: QuickOpenEntry[] = []; - for (let i = 0; i < complete.results.length; i++) { - const fileMatch = complete.results[i]; - - const label = paths.basename(fileMatch.resource.fsPath); - const description = labels.getPathLabel(resources.dirname(fileMatch.resource), this.contextService, this.environmentService); - - results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass)); + return this.getAbsolutePathResult(query).then(result => { + if (token.isCancellationRequested) { + return TPromise.wrap({ results: [] }); } - return new FileQuickOpenModel(results, complete.stats); + // If the original search value is an existing file on disk, return it immediately and bypass the search service + if (result) { + return TPromise.wrap({ results: [{ resource: result }] }); + } + + return this.searchService.search(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); + }).then(complete => { + const results: QuickOpenEntry[] = []; + + if (!token.isCancellationRequested) { + for (let i = 0; i < complete.results.length; i++) { + const fileMatch = complete.results[i]; + + const label = paths.basename(fileMatch.resource.fsPath); + const description = this.labelService.getUriLabel(resources.dirname(fileMatch.resource), true); + + results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass)); + } + } + + return new FileQuickOpenModel(results, complete.stats); }); } - public hasShortResponseTime(): boolean { + private getAbsolutePathResult(query: IPreparedQuery): TPromise { + if (paths.isAbsolute(query.original)) { + const resource = URI.file(query.original); + + return this.fileService.resolveFile(resource).then(stat => stat.isDirectory ? void 0 : resource, error => void 0); + } + + return TPromise.as(null); + } + + private doResolveQueryOptions(query: IPreparedQuery, cacheKey?: string, maxSortedResults?: number): IQueryOptions { + const queryOptions: IQueryOptions = { + extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService), + filePattern: query.value, + cacheKey + }; + + if (typeof maxSortedResults === 'number') { + queryOptions.maxResults = maxSortedResults; + queryOptions.sortByScore = true; + } + + return queryOptions; + } + + hasShortResponseTime(): boolean { return this.isCacheLoaded; } - public onOpen(): void { + onOpen(): void { this.cacheState = new CacheState(cacheKey => this.cacheQuery(cacheKey), query => this.searchService.search(query), cacheKey => this.searchService.clearCache(cacheKey), this.cacheState); this.cacheState.load(); } @@ -205,15 +240,15 @@ export class OpenFileHandler extends QuickOpenHandler { return query; } - public get isCacheLoaded(): boolean { + get isCacheLoaded(): boolean { return this.cacheState && this.cacheState.isLoaded; } - public getGroupLabel(): string { + getGroupLabel(): string { return nls.localize('searchResults', "search results"); } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { return { autoFocusFirstEntry: true }; @@ -251,21 +286,21 @@ export class CacheState { } } - public get cacheKey(): string { + get cacheKey(): string { return this.loadingPhase === LoadingPhase.Loaded || !this.previous ? this._cacheKey : this.previous.cacheKey; } - public get isLoaded(): boolean { + get isLoaded(): boolean { const isLoaded = this.loadingPhase === LoadingPhase.Loaded; return isLoaded || !this.previous ? isLoaded : this.previous.isLoaded; } - public get isUpdating(): boolean { + get isUpdating(): boolean { const isUpdating = this.loadingPhase === LoadingPhase.Loading; return isUpdating || !this.previous ? isUpdating : this.previous.isUpdating; } - public load(): void { + load(): void { if (this.isUpdating) { return; } @@ -283,7 +318,7 @@ export class CacheState { }); } - public dispose(): void { + dispose(): void { if (this.promise) { this.promise.then(null, () => { }) .then(() => { diff --git a/src/vs/workbench/parts/search/browser/openSymbolHandler.ts b/src/vs/workbench/parts/search/browser/openSymbolHandler.ts index c3a855cb222..b44f07d6bab 100644 --- a/src/vs/workbench/parts/search/browser/openSymbolHandler.ts +++ b/src/vs/workbench/parts/search/browser/openSymbolHandler.ts @@ -5,7 +5,7 @@ 'use strict'; import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -15,106 +15,101 @@ import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/comm import * as filters from 'vs/base/common/filters'; import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; -import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import * as labels from 'vs/base/common/labels'; -import { SymbolInformation, symbolKindToCssClass } from 'vs/editor/common/modes'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { symbolKindToCssClass } from 'vs/editor/common/modes'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceSymbolProvider, getWorkspaceSymbols } from 'vs/workbench/parts/search/common/search'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceSymbolProvider, getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search'; import { basename } from 'vs/base/common/paths'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { CancellationToken } from 'vs/base/common/cancellation'; class SymbolEntry extends EditorQuickOpenEntry { - - private _bearingResolve: TPromise; + private bearingResolve: Thenable; constructor( - private _bearing: SymbolInformation, - private _provider: IWorkspaceSymbolProvider, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + private bearing: IWorkspaceSymbol, + private provider: IWorkspaceSymbolProvider, + @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService editorService: IEditorService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService + @ILabelService private labelService: ILabelService ) { super(editorService); } - public getLabel(): string { - return this._bearing.name; + getLabel(): string { + return this.bearing.name; } - public getAriaLabel(): string { + getAriaLabel(): string { return nls.localize('entryAriaLabel', "{0}, symbols picker", this.getLabel()); } - public getDescription(): string { - const containerName = this._bearing.containerName; - if (this._bearing.location.uri) { + getDescription(): string { + const containerName = this.bearing.containerName; + if (this.bearing.location.uri) { if (containerName) { - return `${containerName} — ${basename(this._bearing.location.uri.fsPath)}`; - } else { - return labels.getPathLabel(this._bearing.location.uri, this._contextService, this._environmentService); + return `${containerName} — ${basename(this.bearing.location.uri.fsPath)}`; } + + return this.labelService.getUriLabel(this.bearing.location.uri, true); } + return containerName; } - public getIcon(): string { - return symbolKindToCssClass(this._bearing.kind); + getIcon(): string { + return symbolKindToCssClass(this.bearing.kind); } - public getResource(): URI { - return this._bearing.location.uri; + getResource(): URI { + return this.bearing.location.uri; } - public run(mode: Mode, context: IEntryRunContext): boolean { + run(mode: Mode, context: IEntryRunContext): boolean { // resolve this type bearing if neccessary - if (!this._bearingResolve - && typeof this._provider.resolveWorkspaceSymbol === 'function' - && !this._bearing.location.range - ) { + if (!this.bearingResolve && typeof this.provider.resolveWorkspaceSymbol === 'function' && !this.bearing.location.range) { + this.bearingResolve = Promise.resolve(this.provider.resolveWorkspaceSymbol(this.bearing, CancellationToken.None)).then(result => { + this.bearing = result || this.bearing; - this._bearingResolve = this._provider.resolveWorkspaceSymbol(this._bearing).then(result => { - this._bearing = result || this._bearing; return this; }, onUnexpectedError); } - TPromise.as(this._bearingResolve) - .then(_ => super.run(mode, context)) - .then(undefined, onUnexpectedError); + TPromise.as(this.bearingResolve) + .then(() => super.run(mode, context)) + .then(void 0, onUnexpectedError); // hide if OPEN return mode === Mode.OPEN; } - public getInput(): IResourceInput | EditorInput { - let input: IResourceInput = { - resource: this._bearing.location.uri, + getInput(): IResourceInput { + const input: IResourceInput = { + resource: this.bearing.location.uri, options: { - pinned: !this._configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen + pinned: !this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen } }; - if (this._bearing.location.range) { - input.options.selection = Range.collapseToStart(this._bearing.location.range); + if (this.bearing.location.range) { + input.options.selection = Range.collapseToStart(this.bearing.location.range); } return input; } - public static compare(elementA: SymbolEntry, elementB: SymbolEntry, searchValue: string): number { + static compare(elementA: SymbolEntry, elementB: SymbolEntry, searchValue: string): number { // Sort by Type if name is identical const elementAName = elementA.getLabel().toLowerCase(); const elementBName = elementB.getLabel().toLowerCase(); if (elementAName === elementBName) { - let elementAType = symbolKindToCssClass(elementA._bearing.kind); - let elementBType = symbolKindToCssClass(elementB._bearing.kind); + let elementAType = symbolKindToCssClass(elementA.bearing.kind); + let elementBType = symbolKindToCssClass(elementB.bearing.kind); return elementAType.localeCompare(elementBType); } @@ -130,9 +125,9 @@ export interface IOpenSymbolOptions { export class OpenSymbolHandler extends QuickOpenHandler { - public static readonly ID = 'workbench.picker.symbols'; + static readonly ID = 'workbench.picker.symbols'; - private static readonly SEARCH_DELAY = 500; // This delay accommodates for the user typing a word and then stops typing to start searching + private static readonly TYPING_SEARCH_DELAY = 200; // This delay accommodates for the user typing a word and then stops typing to start searching private delayer: ThrottledDelayer; private options: IOpenSymbolOptions; @@ -140,33 +135,43 @@ export class OpenSymbolHandler extends QuickOpenHandler { constructor(@IInstantiationService private instantiationService: IInstantiationService) { super(); - this.delayer = new ThrottledDelayer(OpenSymbolHandler.SEARCH_DELAY); + this.delayer = new ThrottledDelayer(OpenSymbolHandler.TYPING_SEARCH_DELAY); this.options = Object.create(null); } - public setOptions(options: IOpenSymbolOptions) { + setOptions(options: IOpenSymbolOptions) { this.options = options; } - public canRun(): boolean | string { + canRun(): boolean | string { return true; } - public getResults(searchValue: string): TPromise { + getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); let promise: TPromise; if (!this.options.skipDelay) { - promise = this.delayer.trigger(() => this.doGetResults(searchValue)); // Run search with delay as needed + promise = this.delayer.trigger(() => { + if (token.isCancellationRequested) { + return TPromise.wrap([]); + } + + return this.doGetResults(searchValue, token); + }); } else { - promise = this.doGetResults(searchValue); + promise = this.doGetResults(searchValue, token); } return promise.then(e => new QuickOpenModel(e)); } - private doGetResults(searchValue: string): TPromise { - return getWorkspaceSymbols(searchValue).then(tuples => { + private doGetResults(searchValue: string, token: CancellationToken): TPromise { + return getWorkspaceSymbols(searchValue, token).then(tuples => { + if (token.isCancellationRequested) { + return []; + } + const result: SymbolEntry[] = []; for (let tuple of tuples) { const [provider, bearings] = tuple; @@ -177,13 +182,13 @@ export class OpenSymbolHandler extends QuickOpenHandler { if (!this.options.skipSorting) { searchValue = searchValue ? strings.stripWildcards(searchValue.toLowerCase()) : searchValue; return result.sort((a, b) => SymbolEntry.compare(a, b, searchValue)); - } else { - return result; } + + return result; }); } - private fillInSymbolEntries(bucket: SymbolEntry[], provider: IWorkspaceSymbolProvider, types: SymbolInformation[], searchValue: string): void { + private fillInSymbolEntries(bucket: SymbolEntry[], provider: IWorkspaceSymbolProvider, types: IWorkspaceSymbol[], searchValue: string): void { // Convert to Entries for (let element of types) { @@ -197,18 +202,18 @@ export class OpenSymbolHandler extends QuickOpenHandler { } } - public getGroupLabel(): string { + getGroupLabel(): string { return nls.localize('symbols', "symbol results"); } - public getEmptyLabel(searchString: string): string { + getEmptyLabel(searchString: string): string { if (searchString.length > 0) { return nls.localize('noSymbolsMatching', "No symbols matching"); } return nls.localize('noSymbolsWithoutInput', "Type to search for symbols"); } - public getAutoFocus(searchValue: string): IAutoFocus { + getAutoFocus(searchValue: string): IAutoFocus { return { autoFocusFirstEntry: true, autoFocusPrefixMatch: searchValue.trim() diff --git a/src/vs/workbench/parts/search/browser/patternInputWidget.ts b/src/vs/workbench/parts/search/browser/patternInputWidget.ts index 522be3a0723..ced2a9bbba2 100644 --- a/src/vs/workbench/parts/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/parts/search/browser/patternInputWidget.ts @@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; -import { ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/input'; +import { ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export interface IOptions { @@ -130,10 +130,7 @@ export class PatternInputWidget extends Widget { } public onSearchSubmit(): void { - const value = this.getValue(); - if (value) { - this.inputBox.addToHistory(value); - } + this.inputBox.addToHistory(); } public showNextTerm() { @@ -215,17 +212,17 @@ export class ExcludePatternInputWidget extends PatternInputWidget { } protected renderSubcontrols(controlsDiv: HTMLDivElement): void { - this.useExcludesAndIgnoreFilesBox = new Checkbox({ + this.useExcludesAndIgnoreFilesBox = this._register(new Checkbox({ actionClassName: 'useExcludesAndIgnoreFiles', title: nls.localize('useExcludesAndIgnoreFilesDescription', "Use Exclude Settings and Ignore Files"), isChecked: true, - onChange: (viaKeyboard) => { - this.onOptionChange(null); - if (!viaKeyboard) { - this.inputBox.focus(); - } + })); + this._register(this.useExcludesAndIgnoreFilesBox.onChange(viaKeyboard => { + this.onOptionChange(null); + if (!viaKeyboard) { + this.inputBox.focus(); } - }); + })); this._register(attachCheckboxStyler(this.useExcludesAndIgnoreFilesBox, this.themeService)); controlsDiv.appendChild(this.useExcludesAndIgnoreFilesBox.domNode); diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index 73fe8903f8a..975d8f3f1e1 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as network from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; @@ -24,6 +24,9 @@ import { ResourceTextEdit } from 'vs/editor/common/modes'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { Range } from 'vs/editor/common/core/range'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { mergeSort } from 'vs/base/common/arrays'; const REPLACE_PREVIEW = 'replacePreview'; @@ -103,26 +106,7 @@ export class ReplaceService implements IReplaceService { public replace(match: FileMatchOrMatch, progress?: IProgressRunner, resource?: URI): TPromise; public replace(arg: any, progress: IProgressRunner = null, resource: URI = null): TPromise { - const edits: ResourceTextEdit[] = []; - - if (arg instanceof Match) { - let match = arg; - edits.push(this.createEdit(match, match.replaceString, resource)); - } - - if (arg instanceof FileMatch) { - arg = [arg]; - } - - if (arg instanceof Array) { - arg.forEach(element => { - let fileMatch = element; - if (fileMatch.count() > 0) { - edits.push(...fileMatch.matches().map(match => this.createEdit(match, match.replaceString, resource))); - } - }); - } - + const edits: ResourceTextEdit[] = this.createEdits(arg, resource); return this.bulkEditorService.apply({ edits }, { progress }).then(() => this.textFileService.saveAll(edits.map(e => e.resource))); } @@ -140,6 +124,12 @@ export class ReplaceService implements IReplaceService { revealIfVisible: true } }).then(editor => { + const disposable = fileMatch.onDispose(() => { + if (editor && editor.input) { + editor.input.dispose(); + } + disposable.dispose(); + }); this.updateReplacePreview(fileMatch).then(() => { let editorControl = editor.getControl(); if (element instanceof Match) { @@ -163,7 +153,7 @@ export class ReplaceService implements IReplaceService { } else { replaceModel.undo(); } - returnValue = this.replace(fileMatch, null, replacePreviewUri); + this.applyEditsToPreview(fileMatch, replaceModel); } return returnValue.then(() => { sourceModelRef.dispose(); @@ -172,6 +162,42 @@ export class ReplaceService implements IReplaceService { }); } + private applyEditsToPreview(fileMatch: FileMatch, replaceModel: ITextModel): void { + const resourceEdits = this.createEdits(fileMatch, replaceModel.uri); + const modelEdits = []; + for (const resourceEdit of resourceEdits) { + for (const edit of resourceEdit.edits) { + const range = Range.lift(edit.range); + modelEdits.push(EditOperation.replaceMove(range, edit.text)); + } + } + replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []); + } + + private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI = null): ResourceTextEdit[] { + const edits: ResourceTextEdit[] = []; + + if (arg instanceof Match) { + let match = arg; + edits.push(this.createEdit(match, match.replaceString, resource)); + } + + if (arg instanceof FileMatch) { + arg = [arg]; + } + + if (arg instanceof Array) { + arg.forEach(element => { + let fileMatch = element; + if (fileMatch.count() > 0) { + edits.push(...fileMatch.matches().map(match => this.createEdit(match, match.replaceString, resource))); + } + }); + } + + return edits; + } + private createEdit(match: Match, text: string, resource: URI = null): ResourceTextEdit { let fileMatch: FileMatch = match.parent(); let resourceEdit: ResourceTextEdit = { diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index c9d05ffe457..24a3c06a999 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -7,16 +7,15 @@ import * as DOM from 'vs/base/browser/dom'; import { Action } from 'vs/base/common/actions'; import { INavigator } from 'vs/base/common/iterator'; import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { getPathLabel } from 'vs/base/common/labels'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { isWindows, OS } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ISearchHistoryService, VIEW_ID } from 'vs/platform/search/common/search'; @@ -27,9 +26,7 @@ import { FileMatch, FileMatchOrMatch, FolderMatch, Match, RenderableMatch, searc import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { showDeprecatedWarning } from 'vs/platform/widget/browser/input'; +import { normalize } from 'vs/base/common/paths'; export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean { let searchView = getSearchView(viewletService, panelService); @@ -87,228 +84,57 @@ export const toggleRegexCommand = (accessor: ServicesAccessor) => { searchView.toggleRegex(); }; -export class ShowNextSearchIncludeAction extends Action { +export class FocusNextInputAction extends Action { - public static readonly ID = 'search.history.showNextIncludePattern'; - public static readonly LABEL = nls.localize('nextSearchIncludePattern', "Show Next Search Include Pattern"); + public static readonly ID = 'search.focus.nextInputBox'; constructor(id: string, label: string, @IViewletService private viewletService: IViewletService, - @IPanelService private panelService: IPanelService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IKeybindingService private keybindingService: IKeybindingService, - @INotificationService private notificationService: INotificationService, - @IStorageService private storageService: IStorageService + @IPanelService private panelService: IPanelService ) { super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); } public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchIncludePattern.showNextTerm(); + searchView.focusNextInputBox(); return TPromise.as(null); } } -export class ShowPreviousSearchIncludeAction extends Action { +export class FocusPreviousInputAction extends Action { - public static readonly ID = 'search.history.showPreviousIncludePattern'; - public static readonly LABEL = nls.localize('previousSearchIncludePattern', "Show Previous Search Include Pattern"); + public static readonly ID = 'search.focus.previousInputBox'; constructor(id: string, label: string, @IViewletService private viewletService: IViewletService, - @IPanelService private panelService: IPanelService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService + @IPanelService private panelService: IPanelService ) { super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); } public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchIncludePattern.showPreviousTerm(); + searchView.focusPreviousInputBox(); return TPromise.as(null); } } -export class ShowNextSearchExcludeAction extends Action { - - public static readonly ID = 'search.history.showNextExcludePattern'; - public static readonly LABEL = nls.localize('nextSearchExcludePattern', "Show Next Search Exclude Pattern"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IPanelService private panelService: IPanelService, - @IContextKeyService private contextKeyService: IContextKeyService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchExcludePattern.showNextTerm(); - return TPromise.as(null); - } -} - -export class ShowPreviousSearchExcludeAction extends Action { - - public static readonly ID = 'search.history.showPreviousExcludePattern'; - public static readonly LABEL = nls.localize('previousSearchExcludePattern', "Show Previous Search Exclude Pattern"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchExcludePattern.showPreviousTerm(); - return TPromise.as(null); - } -} - -export class ShowNextSearchTermAction extends Action { - - public static readonly ID = 'search.history.showNext'; - public static readonly LABEL = nls.localize('nextSearchTerm', "Show Next Search Term"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchAndReplaceWidget.showNextSearchTerm(); - return TPromise.as(null); - } -} - -export class ShowPreviousSearchTermAction extends Action { - - public static readonly ID = 'search.history.showPrevious'; - public static readonly LABEL = nls.localize('previousSearchTerm', "Show Previous Search Term"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchAndReplaceWidget.showPreviousSearchTerm(); - return TPromise.as(null); - } -} -export class ShowNextReplaceTermAction extends Action { - - public static readonly ID = 'search.replaceHistory.showNext'; - public static readonly LABEL = nls.localize('nextReplaceTerm', "Show Next Search Replace Term"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchAndReplaceWidget.showNextReplaceTerm(); - return TPromise.as(null); - } -} - -export class ShowPreviousReplaceTermAction extends Action { - - public static readonly ID = 'search.replaceHistory.showPrevious'; - public static readonly LABEL = nls.localize('previousReplaceTerm', "Show Previous Search Replace Term"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService, - @INotificationService private notificationService: INotificationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IStorageService private storageService: IStorageService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - showDeprecatedWarning(this.notificationService, this.keybindingService, this.storageService); - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchAndReplaceWidget.showPreviousReplaceTerm(); - return TPromise.as(null); - } -} - -export const FocusActiveEditorCommand = (accessor: ServicesAccessor) => { - const editorService = accessor.get(IEditorService); - const activeControl = editorService.activeControl; - if (activeControl) { - activeControl.focus(); - } - return TPromise.as(true); -}; - export abstract class FindOrReplaceInFilesAction extends Action { constructor(id: string, label: string, private viewletService: IViewletService, private panelService: IPanelService, - private expandSearchReplaceWidget: boolean, private selectWidgetText: boolean, private focusReplace: boolean) { + private expandSearchReplaceWidget: boolean + ) { super(id, label); } public run(): TPromise { - return openSearchView(this.viewletService, this.panelService, true).then(openedView => { + return openSearchView(this.viewletService, this.panelService, false).then(openedView => { const searchAndReplaceWidget = openedView.searchAndReplaceWidget; searchAndReplaceWidget.toggleReplace(this.expandSearchReplaceWidget); - // Focus replace only when there is text in the searchInput box - const focusReplace = this.focusReplace && searchAndReplaceWidget.searchInput.getValue(); - searchAndReplaceWidget.focus(this.selectWidgetText, !!focusReplace); + + const updatedText = openedView.updateTextFromSelection(!this.expandSearchReplaceWidget); + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); }); } } @@ -321,7 +147,7 @@ export class FindInFilesAction extends FindOrReplaceInFilesAction { @IViewletService viewletService: IViewletService, @IPanelService panelService: IPanelService ) { - super(id, label, viewletService, panelService, /*expandSearchReplaceWidget=*/false, /*selectWidgetText=*/true, /*focusReplace=*/false); + super(id, label, viewletService, panelService, /*expandSearchReplaceWidget=*/false); } } @@ -334,7 +160,7 @@ export class ReplaceInFilesAction extends FindOrReplaceInFilesAction { @IViewletService viewletService: IViewletService, @IPanelService panelService: IPanelService ) { - super(id, label, viewletService, panelService, /*expandSearchReplaceWidget=*/true, /*selectWidgetText=*/false, /*focusReplace=*/true); + super(id, label, viewletService, panelService, /*expandSearchReplaceWidget=*/true); } } @@ -408,7 +234,34 @@ export class CollapseDeepestExpandedLevelAction extends Action { return TPromise.as(null); // Global action disabled if user is in edit mode from another action } - viewer.collapseDeepestExpandedLevel(); + /** + * The hierarchy is FolderMatch, FileMatch, Match. If the top level is FileMatches, then there is only + * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, + * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. + */ + const navigator = viewer.getNavigator(); + let node = navigator.first(); + let collapseFileMatchLevel = false; + if (node instanceof FolderMatch) { + while (node = navigator.next()) { + if (node instanceof Match) { + collapseFileMatchLevel = true; + break; + } + } + } + + if (collapseFileMatchLevel) { + node = navigator.first(); + do { + if (node instanceof FileMatch) { + viewer.collapse(node); + } + } while (node = navigator.next()); + } else { + viewer.collapseAll(); + } + viewer.clearSelection(); viewer.clearFocus(); viewer.domFocus(); @@ -421,7 +274,7 @@ export class CollapseDeepestExpandedLevelAction extends Action { export class ClearSearchResultsAction extends Action { static readonly ID: string = 'search.action.clearSearchResults'; - static LABEL: string = nls.localize('ClearSearchResultsAction.label', "Clear"); + static LABEL: string = nls.localize('ClearSearchResultsAction.label', "Clear Search Results"); constructor(id: string, label: string, @IViewletService private viewletService: IViewletService, @@ -658,14 +511,15 @@ export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction { super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), 'action-replace-all'); } - public async run(): TPromise { + public run(): TPromise { let nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, this.folderMatch); - await this.folderMatch.replaceAll(); - - if (nextFocusElement) { - this.viewer.setFocus(nextFocusElement); - } - this.viewer.domFocus(); + return this.folderMatch.replaceAll() + .then(() => { + if (nextFocusElement) { + this.viewer.setFocus(nextFocusElement); + } + this.viewer.domFocus(); + }); } } @@ -751,7 +605,7 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { } function uriToClipboardString(resource: URI): string { - return resource.scheme === Schemas.file ? getPathLabel(resource) : resource.toString(); + return resource.scheme === Schemas.file ? normalize(normalizeDriveLetter(resource.fsPath), true) : resource.toString(); } export const copyPathCommand: ICommandHandler = (accessor, fileMatch: FileMatch | FolderMatch) => { @@ -845,3 +699,11 @@ export const clearHistoryCommand: ICommandHandler = accessor => { const searchHistoryService = accessor.get(ISearchHistoryService); searchHistoryService.clearHistory(); }; + +export const focusSearchListCommand: ICommandHandler = accessor => { + const viewletService = accessor.get(IViewletService); + const panelService = accessor.get(IPanelService); + openSearchView(viewletService, panelService).then(searchView => { + searchView.moveFocusToResults(); + }); +}; diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index f4fffc73e4a..88e4943184b 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -21,13 +21,13 @@ import { RemoveAction, ReplaceAllAction, ReplaceAction, ReplaceAllInFolderAction import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { getPathLabel } from 'vs/base/common/labels'; import { FileKind } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { WorkbenchTreeController, WorkbenchTree } from 'vs/platform/list/browser/listService'; import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { ILabelService } from 'vs/platform/label/common/label'; export class SearchDataSource implements IDataSource { @@ -36,7 +36,10 @@ export class SearchDataSource implements IDataSource { private includeFolderMatch: boolean; private listener: IDisposable; - constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { + constructor( + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IConfigurationService private configurationService: IConfigurationService, + ) { this.updateIncludeFolderMatch(); this.listener = this.contextService.onDidChangeWorkbenchState(() => this.updateIncludeFolderMatch()); } @@ -103,6 +106,14 @@ export class SearchDataSource implements IDataSource { if (numChildren <= 0) { return false; } + + const collapseOption = this.configurationService.getValue('search.collapseResults'); + if (collapseOption === 'alwaysCollapse') { + return false; + } else if (collapseOption === 'alwaysExpand') { + return true; + } + return numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT || element instanceof FolderMatch; } @@ -235,12 +246,13 @@ export class SearchRenderer extends Disposable implements IRenderer { } private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void { - if (folderMatch.hasRoot()) { - const fileKind = resources.isEqual(this.contextService.getWorkspaceFolder(folderMatch.resource()).uri, folderMatch.resource()) ? - FileKind.ROOT_FOLDER : - FileKind.FOLDER; - - templateData.label.setFile(folderMatch.resource(), { fileKind }); + if (folderMatch.hasResource()) { + const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource()); + if (workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource())) { + templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.ROOT_FOLDER, hidePath: true }); + } else { + templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.FOLDER }); + } } else { templateData.label.setValue(nls.localize('searchFolderMatch.other.label', "Other files")); } @@ -261,10 +273,8 @@ export class SearchRenderer extends Disposable implements IRenderer { } private renderFileMatch(tree: ITree, fileMatch: FileMatch, templateData: IFileMatchTemplate): void { - const folderMatch = fileMatch.parent(); - const root = folderMatch.hasRoot() ? folderMatch.resource() : undefined; templateData.el.setAttribute('data-resource', fileMatch.resource().toString()); - templateData.label.setFile(fileMatch.resource(), { root }); + templateData.label.setFile(fileMatch.resource(), { hideIcon: false }); let 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)); @@ -318,16 +328,20 @@ export class SearchRenderer extends Disposable implements IRenderer { export class SearchAccessibilityProvider implements IAccessibilityProvider { - constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) { + constructor( + @ILabelService private labelService: ILabelService + ) { } public getAriaLabel(tree: ITree, element: FileMatchOrMatch): string { if (element instanceof FolderMatch) { - return nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name()); + return element.hasResource() ? + nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name()) : + nls.localize('otherFilesAriaLabel', "{0} matches outside of the workspace, Search result", element.count()); } if (element instanceof FileMatch) { - const path = getPathLabel(element.resource(), this.contextService) || element.resource().fsPath; + const path = this.labelService.getUriLabel(element.resource(), true) || element.resource().fsPath; return nls.localize('fileMatchAriaLabel', "{0} matches in file {1} of folder {2}, Search result", element.count(), element.name(), paths.dirname(path)); } diff --git a/src/vs/workbench/parts/search/browser/searchView.ts b/src/vs/workbench/parts/search/browser/searchView.ts index 3782699ea9a..f65be147777 100644 --- a/src/vs/workbench/parts/search/browser/searchView.ts +++ b/src/vs/workbench/parts/search/browser/searchView.ts @@ -5,7 +5,6 @@ 'use strict'; -import { $, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; @@ -14,14 +13,15 @@ import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; -import { debounceEvent, Emitter } from 'vs/base/common/event'; +import { anyEvent, debounceEvent, Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/paths'; import * as env from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFocusEvent, ITree } from 'vs/base/parts/tree/browser/tree'; +import { ITree } from 'vs/base/parts/tree/browser/tree'; import 'vs/css!./media/searchview'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -47,7 +47,6 @@ import { Viewlet } from 'vs/workbench/browser/viewlet'; import { Scope } from 'vs/workbench/common/memento'; import { IPanel } from 'vs/workbench/common/panel'; import { IViewlet } from 'vs/workbench/common/viewlet'; -import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/parts/search/browser/patternInputWidget'; import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction } from 'vs/workbench/parts/search/browser/searchActions'; import { SearchAccessibilityProvider, SearchDataSource, SearchFilter, SearchRenderer, SearchSorter, SearchTreeController } from 'vs/workbench/parts/search/browser/searchResultsView'; @@ -59,9 +58,11 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/parts/search/comm import { FileMatch, FileMatchOrMatch, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, SearchModel } from 'vs/workbench/parts/search/common/searchModel'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +const $ = dom.$; + export class SearchView extends Viewlet implements IViewlet, IPanel { private static readonly MAX_TEXT_RESULTS = 10000; @@ -93,23 +94,25 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private actions: (RefreshAction | CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | CancelSearchAction)[] = []; private tree: WorkbenchTree; private viewletSettings: any; - private messages: Builder; - private searchWidgetsContainer: Builder; + private messagesElement: HTMLElement; + private messageDisposables: IDisposable[] = []; + private searchWidgetsContainerElement: HTMLElement; private searchWidget: SearchWidget; private size: dom.Dimension; private queryDetails: HTMLElement; private toggleQueryDetailsButton: HTMLElement; private inputPatternExcludes: ExcludePatternInputWidget; private inputPatternIncludes: PatternInputWidget; - private results: Builder; + private resultsElement: HTMLElement; private currentSelectedFileMatch: FileMatch; private readonly selectCurrentMatchEmitter: Emitter; private delayedRefresh: Delayer; private changedWhileHidden: boolean; + private isWide: boolean; - private searchWithoutFolderMessageBuilder: Builder; + private searchWithoutFolderMessageElement: HTMLElement; constructor( @IPartService partService: IPartService, @@ -149,9 +152,9 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); this.viewletSettings = this.getMemento(storageService, Scope.WORKSPACE); - this.toUnbind.push(this.fileService.onFileChanges(e => this.onFilesChanged(e))); - this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); + this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); + this._register(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory())); this.selectCurrentMatchEmitter = new Emitter(); @@ -162,33 +165,26 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } private onDidChangeWorkbenchState(): void { - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageBuilder) { - this.searchWithoutFolderMessageBuilder.hide(); + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) { + dom.hide(this.searchWithoutFolderMessageElement); } } public create(parent: HTMLElement): TPromise { super.create(parent); - this.viewModel = this.searchWorkbenchService.searchModel; - let builder: Builder; - $(parent).div({ - 'class': 'search-view' - }, (div) => { - builder = div; - }); + this.viewModel = this._register(this.searchWorkbenchService.searchModel); + const containerElement = dom.append(parent, $('.search-view')); - builder.div({ 'class': ['search-widgets-container'] }, (div) => { - this.searchWidgetsContainer = div; - }); - this.createSearchWidget(this.searchWidgetsContainer); + this.searchWidgetsContainerElement = dom.append(containerElement, $('.search-widgets-container')); + this.createSearchWidget(this.searchWidgetsContainerElement); const history = this.searchHistoryService.load(); const filePatterns = this.viewletSettings['query.filePatterns'] || ''; let patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; - const patternExclusionsHistory: string[] = history.exclude || []; + const patternExclusionsHistory: string[] = history.exclude || this.viewletSettings['query.folderExclusionsHistory'] || []; let patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; - let patternIncludesHistory: string[] = history.include || []; + let patternIncludesHistory: string[] = history.include || this.viewletSettings['query.folderIncludesHistory'] || []; const queryDetailsExpanded = this.viewletSettings['query.queryDetailsExpanded'] || ''; const useExcludesAndIgnoreFiles = typeof this.viewletSettings['query.useExcludesAndIgnoreFiles'] === 'boolean' ? this.viewletSettings['query.useExcludesAndIgnoreFiles'] : true; @@ -222,84 +218,86 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - this.queryDetails = this.searchWidgetsContainer.div({ 'class': ['query-details'] }, (builder) => { - this.toggleQueryDetailsButton = builder.div({ 'class': 'more', 'tabindex': 0, 'role': 'button', 'title': nls.localize('moreSearch', "Toggle Search Details") }) - .on(dom.EventType.CLICK, (e) => { - dom.EventHelper.stop(e); - this.toggleQueryDetails(); - }).on(dom.EventType.KEY_UP, (e: KeyboardEvent) => { - let event = new StandardKeyboardEvent(e); + this.queryDetails = dom.append(this.searchWidgetsContainerElement, $('.query-details')); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - dom.EventHelper.stop(e); - this.toggleQueryDetails(false); - } - }).on(dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - let event = new StandardKeyboardEvent(e); + // Toggle query details button + this.toggleQueryDetailsButton = dom.append(this.queryDetails, + $('.more', { tabindex: 0, role: 'button', title: nls.localize('moreSearch', "Toggle Search Details") })); - if (event.equals(KeyMod.Shift | KeyCode.Tab)) { - if (this.searchWidget.isReplaceActive()) { - this.searchWidget.focusReplaceAllAction(); - } else { - this.searchWidget.focusRegexAction(); - } - dom.EventHelper.stop(e); - } - }).getHTMLElement(); + this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { + dom.EventHelper.stop(e); + this.toggleQueryDetails(); + })); + this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); - //folder includes list - builder.div({ 'class': 'file-types includes' }, (builder) => { - let title = nls.localize('searchScope.includes', "files to include"); - builder.element('h4', { text: title }); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + dom.EventHelper.stop(e); + this.toggleQueryDetails(false); + } + })); + this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); - this.inputPatternIncludes = this.instantiationService.createInstance(PatternInputWidget, builder.getContainer(), this.contextViewService, { - ariaLabel: nls.localize('label.includes', 'Search Include Patterns'), - history: patternIncludesHistory, - }); + if (event.equals(KeyMod.Shift | KeyCode.Tab)) { + if (this.searchWidget.isReplaceActive()) { + this.searchWidget.focusReplaceAllAction(); + } else { + this.searchWidget.focusRegexAction(); + } + dom.EventHelper.stop(e); + } + })); - this.inputPatternIncludes.setValue(patternIncludes); + // folder includes list + const folderIncludesList = dom.append(this.queryDetails, + $('.file-types.includes')); + const filesToIncludeTitle = nls.localize('searchScope.includes', "files to include"); + dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle)); - this.inputPatternIncludes - .on(FindInput.OPTION_CHANGE, (e) => { - this.onQueryChanged(false); - }); + this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, { + ariaLabel: nls.localize('label.includes', 'Search Include Patterns'), + history: patternIncludesHistory, + })); - this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true, true)); - this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget - this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); + this.inputPatternIncludes.setValue(patternIncludes); + + this.inputPatternIncludes.on(FindInput.OPTION_CHANGE, (e) => { + this.onQueryChanged(false); + }); + + this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true, true)); + this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget + this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); + + // excludes list + const excludesList = dom.append(this.queryDetails, $('.file-types.excludes')); + const excludesTitle = nls.localize('searchScope.excludes', "files to exclude"); + dom.append(excludesList, $('h4', undefined, excludesTitle)); + this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, { + ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns'), + history: patternExclusionsHistory, + })); + + this.inputPatternExcludes.setValue(patternExclusions); + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); + + this.inputPatternExcludes + .on(FindInput.OPTION_CHANGE, (e) => { + this.onQueryChanged(false); }); - //pattern exclusion list - builder.div({ 'class': 'file-types excludes' }, (builder) => { - let title = nls.localize('searchScope.excludes', "files to exclude"); - builder.element('h4', { text: title }); + this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); + this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); + this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget + this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused); - this.inputPatternExcludes = this.instantiationService.createInstance(ExcludePatternInputWidget, builder.getContainer(), this.contextViewService, { - ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns'), - history: patternExclusionsHistory, - }); - - this.inputPatternExcludes.setValue(patternExclusions); - this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); - - this.inputPatternExcludes - .on(FindInput.OPTION_CHANGE, (e) => { - this.onQueryChanged(false); - }); - - this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); - this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); - this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget - this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused); - }); - }).getHTMLElement(); - - this.messages = builder.div({ 'class': 'messages' }).hide().clone(); + this.messagesElement = dom.append(containerElement, $('.messages')); if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.searchWithoutFolderMessage(this.clearMessage()); + this.showSearchWithoutFolderMessage(); } - this.createSearchResultsView(builder); + this.createSearchResultsView(containerElement); this.actions = [ this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL), @@ -311,7 +309,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.toggleQueryDetails(true, true, true); } - this.toUnbind.push(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event))); + this._register(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event))); return TPromise.as(null); } @@ -334,61 +332,59 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - private createSearchWidget(builder: Builder): void { + private createSearchWidget(container: HTMLElement): void { let contentPattern = this.viewletSettings['query.contentPattern'] || ''; let isRegex = this.viewletSettings['query.regex'] === true; let isWholeWords = this.viewletSettings['query.wholeWords'] === true; let isCaseSensitive = this.viewletSettings['query.caseSensitive'] === true; const history = this.searchHistoryService.load(); - let searchHistory = history.search || []; - let replaceHistory = history.replace || []; + let searchHistory = history.search || this.viewletSettings['query.searchHistory'] || []; + let replaceHistory = history.replace || this.viewletSettings['query.replaceHistory'] || []; - this.searchWidget = this.instantiationService.createInstance(SearchWidget, builder, { + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { value: contentPattern, isRegex: isRegex, isCaseSensitive: isCaseSensitive, isWholeWords: isWholeWords, searchHistory: searchHistory, replaceHistory: replaceHistory - }); + })); if (this.storageService.getBoolean(SearchView.SHOW_REPLACE_STORAGE_KEY, StorageScope.WORKSPACE, true)) { this.searchWidget.toggleReplace(true); } - this.toUnbind.push(this.searchWidget); + this._register(this.searchWidget.onSearchSubmit((refresh) => this.onQueryChanged(refresh))); + this._register(this.searchWidget.onSearchCancel(() => this.cancelSearch())); + this._register(this.searchWidget.searchInput.onDidOptionChange((viaKeyboard) => this.onQueryChanged(true, viaKeyboard))); - this.toUnbind.push(this.searchWidget.onSearchSubmit((refresh) => this.onQueryChanged(refresh))); - this.toUnbind.push(this.searchWidget.onSearchCancel(() => this.cancelSearch())); - this.toUnbind.push(this.searchWidget.searchInput.onDidOptionChange((viaKeyboard) => this.onQueryChanged(true, viaKeyboard))); - - this.toUnbind.push(this.searchWidget.onReplaceToggled(() => this.onReplaceToggled())); - this.toUnbind.push(this.searchWidget.onReplaceStateChange((state) => { + this._register(this.searchWidget.onReplaceToggled(() => this.onReplaceToggled())); + this._register(this.searchWidget.onReplaceStateChange((state) => { this.viewModel.replaceActive = state; this.tree.refresh(); })); - this.toUnbind.push(this.searchWidget.onReplaceValueChanged((value) => { + this._register(this.searchWidget.onReplaceValueChanged((value) => { this.viewModel.replaceString = this.searchWidget.getReplaceValue(); this.delayedRefresh.trigger(() => this.tree.refresh()); })); - this.toUnbind.push(this.searchWidget.onBlur(() => { + this._register(this.searchWidget.onBlur(() => { this.toggleQueryDetailsButton.focus(); })); - this.toUnbind.push(this.searchWidget.onReplaceAll(() => this.replaceAll())); + this._register(this.searchWidget.onReplaceAll(() => this.replaceAll())); this.trackInputBox(this.searchWidget.searchInputFocusTracker); this.trackInputBox(this.searchWidget.replaceInputFocusTracker); } private trackInputBox(inputFocusTracker: dom.IFocusTracker, contextKey?: IContextKey): void { - this.toUnbind.push(inputFocusTracker.onDidFocus(() => { + this._register(inputFocusTracker.onDidFocus(() => { this.inputBoxFocused.set(true); if (contextKey) { contextKey.set(true); } })); - this.toUnbind.push(inputFocusTracker.onDidBlur(() => { + this._register(inputFocusTracker.onDidBlur(() => { this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() || this.inputPatternIncludes.inputHasFocus() @@ -406,7 +402,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { if (!isReplaceShown) { this.storageService.store(SearchView.SHOW_REPLACE_STORAGE_KEY, false, StorageScope.WORKSPACE); } else { - this.storageService.remove(SearchView.SHOW_REPLACE_STORAGE_KEY); + this.storageService.remove(SearchView.SHOW_REPLACE_STORAGE_KEY, StorageScope.WORKSPACE); } } @@ -462,8 +458,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.setReplaceAllActionState(false); this.viewModel.searchResult.replaceAll(progressRunner).then(() => { progressRunner.done(); - this.clearMessage() - .p({ text: afterReplaceAllMessage }); + const messageEl = this.clearMessage(); + dom.append(messageEl, $('p', undefined, afterReplaceAllMessage)); }, (error) => { progressRunner.done(); errors.isPromiseCanceledError(error); @@ -537,85 +533,73 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return nls.localize('replaceAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files?", occurrences, fileCount); } - private clearMessage(): Builder { - this.searchWithoutFolderMessageBuilder = void 0; + private clearMessage(): HTMLElement { + this.searchWithoutFolderMessageElement = void 0; - return this.messages.empty().show() - .asContainer().div({ 'class': 'message' }) - .asContainer(); + dom.clearNode(this.messagesElement); + dom.show(this.messagesElement); + dispose(this.messageDisposables); + this.messageDisposables = []; + + return dom.append(this.messagesElement, $('.message')); } - private createSearchResultsView(builder: Builder): void { - builder.div({ 'class': 'results' }, (div) => { - this.results = div; - this.results.addClass('show-file-icons'); + private createSearchResultsView(container: HTMLElement): void { + this.resultsElement = dom.append(container, $('.results.show-file-icons')); + const dataSource = this._register(this.instantiationService.createInstance(SearchDataSource)); + const renderer = this._register(this.instantiationService.createInstance(SearchRenderer, this.getActionRunner(), this)); + const dnd = this.instantiationService.createInstance(SimpleFileResourceDragAndDrop, (obj: any) => obj instanceof FileMatch ? obj.resource() : void 0); - let dataSource = this.instantiationService.createInstance(SearchDataSource); - this.toUnbind.push(dataSource); + this.tree = this._register(this.instantiationService.createInstance(WorkbenchTree, this.resultsElement, { + dataSource: dataSource, + renderer: renderer, + sorter: new SearchSorter(), + filter: new SearchFilter(), + controller: this.instantiationService.createInstance(SearchTreeController), + accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider), + dnd + }, { + ariaLabel: nls.localize('treeAriaLabel', "Search Results"), + showLoading: false + })); - let renderer = this.instantiationService.createInstance(SearchRenderer, this.getActionRunner(), this); - this.toUnbind.push(renderer); + this.tree.setInput(this.viewModel.searchResult); - let dnd = this.instantiationService.createInstance(SimpleFileResourceDragAndDrop, (obj: any) => obj instanceof FileMatch ? obj.resource() : void 0); - - this.tree = this.instantiationService.createInstance(WorkbenchTree, div.getHTMLElement(), { - dataSource: dataSource, - renderer: renderer, - sorter: new SearchSorter(), - filter: new SearchFilter(), - controller: this.instantiationService.createInstance(SearchTreeController), - accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider), - dnd - }, { - ariaLabel: nls.localize('treeAriaLabel', "Search Results"), - showLoading: false - }); - - this.tree.setInput(this.viewModel.searchResult); - this.toUnbind.push(renderer); - - const searchResultsNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true })); - this._register(debounceEvent(searchResultsNavigator.openResource, (last, event) => event, 75, true)(options => { - if (options.element instanceof Match) { - let selectedMatch: Match = options.element; - if (this.currentSelectedFileMatch) { - this.currentSelectedFileMatch.setSelectedMatch(null); - } - this.currentSelectedFileMatch = selectedMatch.parent(); - this.currentSelectedFileMatch.setSelectedMatch(selectedMatch); - if (!(options.payload && options.payload.preventEditorOpen)) { - this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned); - } + const searchResultsNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true })); + this._register(debounceEvent(searchResultsNavigator.openResource, (last, event) => event, 75, true)(options => { + if (options.element instanceof Match) { + let selectedMatch: Match = options.element; + if (this.currentSelectedFileMatch) { + this.currentSelectedFileMatch.setSelectedMatch(null); } - })); - - let treeHasFocus = false; - this.tree.onDidFocus(() => { - treeHasFocus = true; - }); - - this.toUnbind.push(this.tree.onDidChangeFocus((e: IFocusEvent) => { - if (treeHasFocus) { - const focus = e.focus; - this.firstMatchFocused.set(this.tree.getNavigator().first() === focus); - this.fileMatchOrMatchFocused.set(!!focus); - this.fileMatchFocused.set(focus instanceof FileMatch); - this.folderMatchFocused.set(focus instanceof FolderMatch); - this.matchFocused.set(focus instanceof Match); - this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); + this.currentSelectedFileMatch = selectedMatch.parent(); + this.currentSelectedFileMatch.setSelectedMatch(selectedMatch); + if (!(options.payload && options.payload.preventEditorOpen)) { + this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned); } - })); + } + })); - this.toUnbind.push(this.tree.onDidBlur(e => { - treeHasFocus = false; - this.firstMatchFocused.reset(); - this.fileMatchOrMatchFocused.reset(); - this.fileMatchFocused.reset(); - this.folderMatchFocused.reset(); - this.matchFocused.reset(); - this.fileMatchOrFolderMatchFocus.reset(); - })); - }); + this._register(anyEvent(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => { + if (this.tree.isDOMFocused()) { + const focus = this.tree.getFocus(); + this.firstMatchFocused.set(this.tree.getNavigator().first() === focus); + this.fileMatchOrMatchFocused.set(!!focus); + this.fileMatchFocused.set(focus instanceof FileMatch); + this.folderMatchFocused.set(focus instanceof FolderMatch); + this.matchFocused.set(focus instanceof Match); + this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); + } + })); + + this._register(this.tree.onDidBlur(e => { + this.firstMatchFocused.reset(); + this.fileMatchOrMatchFocused.reset(); + this.fileMatchFocused.reset(); + this.folderMatchFocused.reset(); + this.matchFocused.reset(); + this.fileMatchOrFolderMatchFocus.reset(); + })); } public selectCurrentMatch(): void { @@ -739,13 +723,22 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return promise; } + public moveFocusToResults(): void { + this.tree.domFocus(); + } + public focus(): void { super.focus(); + const updatedText = this.updateTextFromSelection(); + this.searchWidget.focus(undefined, undefined, updatedText); + } + + public updateTextFromSelection(allowUnselectedWord = true): boolean { let updatedText = false; const seedSearchStringFromSelection = this.configurationService.getValue('editor').find.seedSearchStringFromSelection; if (seedSearchStringFromSelection) { - let selectedText = this.getSearchTextFromEditor(); + let selectedText = this.getSearchTextFromEditor(allowUnselectedWord); if (selectedText) { if (this.searchWidget.searchInput.getRegex()) { selectedText = strings.escapeRegExpCharacters(selectedText); @@ -756,7 +749,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - this.searchWidget.focus(undefined, undefined, updatedText); + return updatedText; } public focusNextInputBox(): void { @@ -835,8 +828,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (this.size.width >= SearchView.WIDE_VIEW_SIZE) { + this.isWide = true; dom.addClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } else { + this.isWide = false; dom.removeClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } @@ -845,12 +840,15 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); - const messagesSize = this.messages.isHidden() ? 0 : dom.getTotalHeight(this.messages.getHTMLElement()); + const messagesSize = this.messagesElement.style.display === 'none' ? + 0 : + dom.getTotalHeight(this.messagesElement); + const searchResultContainerSize = this.size.height - messagesSize - - dom.getTotalHeight(this.searchWidgetsContainer.getContainer()); + dom.getTotalHeight(this.searchWidgetsContainerElement); - this.results.style({ height: searchResultContainerSize + 'px' }); + this.resultsElement.style.height = searchResultContainerSize + 'px'; this.tree.layout(searchResultContainerSize); } @@ -880,7 +878,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewModel.searchResult.clear(); this.showEmptyStage(); if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.searchWithoutFolderMessage(this.clearMessage()); + this.showSearchWithoutFolderMessage(); } this.searchWidget.clear(); this.viewModel.cancelSearch(); @@ -904,7 +902,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - private getSearchTextFromEditor(): string { + private getSearchTextFromEditor(allowUnselectedWord: boolean): string { if (!this.editorService.activeEditor) { return null; } @@ -927,7 +925,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return null; } - if (range.isEmpty() && !this.searchWidget.searchInput.getValue()) { + if (range.isEmpty() && !this.searchWidget.searchInput.getValue() && allowUnselectedWord) { const wordAtPosition = activeTextEditorWidget.getModel().getWordAtPosition(range.getStartPosition()); if (wordAtPosition) { return wordAtPosition.word; @@ -969,6 +967,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { skipLayout = Boolean(skipLayout); if (show) { + this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true'); dom.addClass(this.queryDetails, cls); if (moveFocus) { if (reverse) { @@ -980,6 +979,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } } else { + this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false'); dom.removeClass(this.queryDetails, cls); if (moveFocus) { this.searchWidget.focus(); @@ -1076,7 +1076,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { isRegExp: isRegex, isCaseSensitive: isCaseSensitive, isWordMatch: isWholeWords, - wordSeparators: this.configurationService.getValue().editor.wordSeparators, isSmartCase: this.configurationService.getValue().search.smartCase }; @@ -1089,7 +1088,12 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { disregardIgnoreFiles: !useExcludesAndIgnoreFiles, disregardExcludeSettings: !useExcludesAndIgnoreFiles, excludePattern, - includePattern + includePattern, + previewOptions: { + leadingChars: 20, + maxLines: 1, + totalChars: this.isWide ? 250 : 75 + } }; const folderResources = this.contextService.getWorkspace().folders; @@ -1182,7 +1186,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } return null; - }).done(null, errors.onUnexpectedError); + }); this.viewModel.replaceString = this.searchWidget.getReplaceValue(); @@ -1219,67 +1223,52 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { aria.status(message); this.tree.onHidden(); - this.results.hide(); - const div = this.clearMessage(); - const p = $(div).p({ text: message }); + dom.hide(this.resultsElement); + + const messageEl = this.clearMessage(); + const p = dom.append(messageEl, $('p', undefined, message)); if (!completed) { - $(p).a({ - 'class': ['pointer', 'prominent'], - text: nls.localize('rerunSearch.message', "Search again") - }).on(dom.EventType.CLICK, (e: MouseEvent) => { + const searchAgainLink = dom.append(p, $('a.pointer.prominent', undefined, nls.localize('rerunSearch.message', "Search again"))); + this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - this.onQueryChanged(true); - }); + })); } else if (hasIncludes || hasExcludes) { - $(p).a({ - 'class': ['pointer', 'prominent'], - 'tabindex': '0', - text: nls.localize('rerunSearchInAll.message', "Search again in all files") - }).on(dom.EventType.CLICK, (e: MouseEvent) => { + const searchAgainLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('rerunSearchInAll.message', "Search again in all files"))); + this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); this.onQueryChanged(true); - }); + })); } else { - $(p).a({ - 'class': ['pointer', 'prominent'], - 'tabindex': '0', - text: nls.localize('openSettings.message', "Open Settings") - }).on(dom.EventType.CLICK, (e: MouseEvent) => { + const openSettingsLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.message', "Open Settings"))); + this.messageDisposables.push(dom.addDisposableListener(openSettingsLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - let editorPromise = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? this.preferencesService.openWorkspaceSettings() : this.preferencesService.openGlobalSettings(); - editorPromise.done(editor => { - if (editor instanceof PreferencesEditor) { - editor.focusSearch('.exclude'); - } - }, errors.onUnexpectedError); - }); + const options: ISettingsEditorOptions = { query: '.exclude' }; + this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? + this.preferencesService.openWorkspaceSettings(undefined, options) : + this.preferencesService.openGlobalSettings(undefined, options); + })); } if (completed) { - $(p).span({ - text: ' - ' - }); + dom.append(p, $('span', undefined, ' - ')); - $(p).a({ - 'class': ['pointer', 'prominent'], - 'tabindex': '0', - text: nls.localize('openSettings.learnMore', "Learn More") - }).on(dom.EventType.CLICK, (e: MouseEvent) => { + const learnMoreLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.learnMore', "Learn More"))); + this.messageDisposables.push(dom.addDisposableListener(learnMoreLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); window.open('https://go.microsoft.com/fwlink/?linkid=853977'); - }); + })); } if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.searchWithoutFolderMessage(div); + this.showSearchWithoutFolderMessage(); } } else { this.viewModel.searchResult.toggleHighlights(true); // show highlights @@ -1349,7 +1338,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const fileCount = this.viewModel.searchResult.fileCount(); if (visibleMatches !== fileCount) { visibleMatches = fileCount; - this.tree.refresh().done(null, errors.onUnexpectedError); + this.tree.refresh(); this.updateSearchResultCount(); } @@ -1360,22 +1349,22 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.setReplaceAllActionState(false); - this.viewModel.search(query).done(onComplete, onError, onProgress); + this.viewModel.search(query, onProgress).then(onComplete, onError); } private updateSearchResultCount(): void { const fileCount = this.viewModel.searchResult.fileCount(); this.hasSearchResultsKey.set(fileCount > 0); - const msgWasHidden = this.messages.isHidden(); + const msgWasHidden = this.messagesElement.style.display === 'none'; if (fileCount > 0) { - const div = this.clearMessage(); - $(div).p({ text: this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount) }); + const messageEl = this.clearMessage(); + dom.append(messageEl, $('p', undefined, this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount))); if (msgWasHidden) { this.reLayout(); } } else if (!msgWasHidden) { - this.messages.hide(); + dom.hide(this.messagesElement); } } @@ -1391,26 +1380,27 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } } - private searchWithoutFolderMessage(div: Builder): void { - this.searchWithoutFolderMessageBuilder = $(div); + private showSearchWithoutFolderMessage(): void { + this.searchWithoutFolderMessageElement = this.clearMessage(); - this.searchWithoutFolderMessageBuilder.p({ text: nls.localize('searchWithoutFolder', "You have not yet opened a folder. Only open files are currently searched - ") }) - .asContainer().a({ - 'class': ['pointer', 'prominent'], - 'tabindex': '0', - text: nls.localize('openFolder', "Open Folder") - }).on(dom.EventType.CLICK, (e: MouseEvent) => { - dom.EventHelper.stop(e, false); + const textEl = dom.append(this.searchWithoutFolderMessageElement, + $('p', undefined, nls.localize('searchWithoutFolder', "You have not yet opened a folder. Only open files are currently searched - "))); - const actionClass = env.isMacintosh ? OpenFileFolderAction : OpenFolderAction; - const action = this.instantiationService.createInstance(actionClass, actionClass.ID, actionClass.LABEL); - this.actionRunner.run(action).done(() => { - action.dispose(); - }, err => { - action.dispose(); - errors.onUnexpectedError(err); - }); + const openFolderLink = dom.append(textEl, + $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder"))); + + this.messageDisposables.push(dom.addDisposableListener(openFolderLink, dom.EventType.CLICK, (e: MouseEvent) => { + dom.EventHelper.stop(e, false); + + const actionClass = env.isMacintosh ? OpenFileFolderAction : OpenFolderAction; + const action = this.instantiationService.createInstance(actionClass, actionClass.ID, actionClass.LABEL); + this.actionRunner.run(action).then(() => { + action.dispose(); + }, err => { + action.dispose(); + errors.onUnexpectedError(err); }); + })); } private showEmptyStage(): void { @@ -1421,8 +1411,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { // clean up ui // this.replaceService.disposeAllReplacePreviews(); - this.messages.hide(); - this.results.show(); + dom.hide(this.messagesElement); + dom.show(this.resultsElement); this.tree.onVisible(); this.currentSelectedFileMatch = null; } @@ -1548,11 +1538,21 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewletSettings['query.folderIncludes'] = patternIncludes; this.viewletSettings['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles; + // Deprecated, remove these memento props a couple releases after 1.25 + const searchHistory = this.searchWidget.getSearchHistory(); + const replaceHistory = this.searchWidget.getReplaceHistory(); + const patternExcludesHistory = this.inputPatternExcludes.getHistory(); + const patternIncludesHistory = this.inputPatternIncludes.getHistory(); + this.viewletSettings['query.searchHistory'] = searchHistory; + this.viewletSettings['query.replaceHistory'] = replaceHistory; + this.viewletSettings['query.folderExclusionsHistory'] = patternExcludesHistory; + this.viewletSettings['query.folderIncludesHistory'] = patternIncludesHistory; + this.searchHistoryService.save({ - search: this.searchWidget.getSearchHistory(), - replace: this.searchWidget.getReplaceHistory(), - exclude: this.inputPatternExcludes.getHistory(), - include: this.inputPatternIncludes.getHistory() + search: searchHistory, + replace: replaceHistory, + exclude: patternExcludesHistory, + include: patternIncludesHistory }); super.shutdown(); @@ -1561,16 +1561,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { public dispose(): void { this.isDisposed = true; - if (this.tree) { - this.tree.dispose(); - } - - this.searchWidget.dispose(); - this.inputPatternIncludes.dispose(); - this.inputPatternExcludes.dispose(); - - this.viewModel.dispose(); - super.dispose(); } } diff --git a/src/vs/workbench/parts/search/browser/searchWidget.ts b/src/vs/workbench/parts/search/browser/searchWidget.ts index f9fe8deed65..fbcafe78b72 100644 --- a/src/vs/workbench/parts/search/browser/searchWidget.ts +++ b/src/vs/workbench/parts/search/browser/searchWidget.ts @@ -14,13 +14,12 @@ import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findI import { IMessage, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Event, Emitter } from 'vs/base/common/event'; -import { Builder } from 'vs/base/browser/builder'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { isSearchViewFocused, appendKeyBindingLabel } from 'vs/workbench/parts/search/browser/searchActions'; import * as Constants from 'vs/workbench/parts/search/common/constants'; @@ -31,7 +30,8 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ISearchConfigurationProperties } from 'vs/platform/search/common/search'; -import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/input'; +import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import { Delayer } from 'vs/base/common/async'; export interface ISearchWidgetOptions { value?: string; @@ -94,6 +94,7 @@ export class SearchWidget extends Widget { private replaceActionBar: ActionBar; public replaceInputFocusTracker: dom.IFocusTracker; private replaceInputBoxFocused: IContextKey; + private _replaceHistoryDelayer: Delayer; private ignoreGlobalFindBufferOnNextFocus = false; private previousGlobalFindBufferValue: string; @@ -120,7 +121,7 @@ export class SearchWidget extends Widget { public readonly onBlur: Event = this._onBlur.event; constructor( - container: Builder, + container: HTMLElement, options: ISearchWidgetOptions, @IContextViewService private contextViewService: IContextViewService, @IThemeService private themeService: IThemeService, @@ -133,6 +134,7 @@ export class SearchWidget extends Widget { this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService); this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService); this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService); + this._replaceHistoryDelayer = new Delayer(500); this.render(container, options); } @@ -225,8 +227,10 @@ export class SearchWidget extends Widget { this.searchInput.focusOnRegex(); } - private render(container: Builder, options: ISearchWidgetOptions): void { - this.domNode = container.div({ 'class': 'search-widget' }).style({ position: 'relative' }).getHTMLElement(); + private render(container: HTMLElement, options: ISearchWidgetOptions): void { + this.domNode = dom.append(container, dom.$('.search-widget')); + this.domNode.style.position = 'relative'; + this.renderToggleReplaceButton(this.domNode); this.renderSearchInput(this.domNode, options); @@ -241,7 +245,9 @@ export class SearchWidget extends Widget { buttonHoverBackground: null }; this.toggleReplaceButton = this._register(new Button(parent, opts)); - this.toggleReplaceButton.icon = 'toggle-replace-button collapse'; + this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false'); + this.toggleReplaceButton.element.classList.add('collapse'); + this.toggleReplaceButton.icon = 'toggle-replace-button'; // TODO@joh need to dispose this listener eventually this.toggleReplaceButton.onDidClick(() => this.onToggleReplaceButton()); this.toggleReplaceButton.element.title = nls.localize('search.replace.toggle.button.title', "Toggle Replace"); @@ -267,13 +273,13 @@ export class SearchWidget extends Widget { this.searchInput.setCaseSensitive(!!options.isCaseSensitive); this.searchInput.setWholeWords(!!options.isWholeWords); this._register(this.onSearchSubmit(() => { - this.searchInput.inputBox.addToHistory(this.searchInput.getValue()); + this.searchInput.inputBox.addToHistory(); })); this.searchInput.onCaseSensitiveKeyDown((keyboardEvent: IKeyboardEvent) => this.onCaseSensitiveKeyDown(keyboardEvent)); this.searchInput.onRegexKeyDown((keyboardEvent: IKeyboardEvent) => this.onRegexKeyDown(keyboardEvent)); this._register(this.onReplaceValueChanged(() => { - this.replaceInput.addToHistory(this.replaceInput.value); + this._replaceHistoryDelayer.trigger(() => this.replaceInput.addToHistory()); })); this.searchInputFocusTracker = this._register(dom.trackFocus(this.searchInput.inputBox.inputElement)); @@ -284,7 +290,7 @@ export class SearchWidget extends Widget { if (!this.ignoreGlobalFindBufferOnNextFocus && useGlobalFindBuffer) { const globalBufferText = this.clipboardServce.readFindText(); if (this.previousGlobalFindBufferValue !== globalBufferText) { - this.searchInput.inputBox.addToHistory(this.searchInput.getValue()); + this.searchInput.inputBox.addToHistory(); this.searchInput.setValue(globalBufferText); this.searchInput.select(); } @@ -315,6 +321,7 @@ export class SearchWidget extends Widget { this.replaceAllAction.label = SearchWidget.REPLACE_ALL_DISABLED_LABEL; this.replaceActionBar = this._register(new ActionBar(this.replaceContainer)); this.replaceActionBar.push([this.replaceAllAction], { icon: true, label: false }); + this.onkeydown(this.replaceActionBar.domNode, (keyboardEvent) => this.onReplaceActionbarKeyDown(keyboardEvent)); this.replaceInputFocusTracker = this._register(dom.trackFocus(this.replaceInput.inputElement)); this._register(this.replaceInputFocusTracker.onDidFocus(() => this.replaceInputBoxFocused.set(true))); @@ -330,6 +337,7 @@ export class SearchWidget extends Widget { dom.toggleClass(this.replaceContainer, 'disabled'); dom.toggleClass(this.toggleReplaceButton.element, 'collapse'); dom.toggleClass(this.toggleReplaceButton.element, 'expand'); + this.toggleReplaceButton.element.setAttribute('aria-expanded', this.isReplaceShown() ? 'true' : 'false'); this.updateReplaceActiveState(); this._onReplaceToggled.fire(); } @@ -376,6 +384,7 @@ export class SearchWidget extends Widget { } private onSearchInputChanged(): void { + this.searchInput.clearMessage(); this.setReplaceAllActionState(false); } @@ -437,6 +446,13 @@ export class SearchWidget extends Widget { } } + private onReplaceActionbarKeyDown(keyboardEvent: IKeyboardEvent) { + if (keyboardEvent.equals(KeyMod.Shift | KeyCode.Tab)) { + this.focusRegexAction(); + keyboardEvent.preventDefault(); + } + } + private submitSearch(refresh: boolean = true): void { const value = this.searchInput.getValue(); const useGlobalFindBuffer = this.configurationService.getValue('search').globalFindClipboard; @@ -460,7 +476,7 @@ export class SearchWidget extends Widget { export function registerContributions() { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: ReplaceAllAction.ID, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, CONTEXT_FIND_WIDGET_NOT_VISIBLE), primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.Enter, handler: accessor => { diff --git a/src/vs/workbench/parts/search/common/constants.ts b/src/vs/workbench/parts/search/common/constants.ts index 39abb302748..1f15c344c0e 100644 --- a/src/vs/workbench/parts/search/common/constants.ts +++ b/src/vs/workbench/parts/search/common/constants.ts @@ -16,6 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath'; export const CopyMatchCommandId = 'search.action.copyMatch'; export const CopyAllCommandId = 'search.action.copyAll'; export const ClearSearchHistoryCommandId = 'search.action.clearHistory'; +export const FocusSearchListCommandID = 'search.action.focusSearchList'; export const ReplaceActionId = 'search.action.replace'; export const ReplaceAllInFileActionId = 'search.action.replaceAllInFile'; export const ReplaceAllInFolderActionId = 'search.action.replaceAllInFolder'; diff --git a/src/vs/workbench/parts/search/common/queryBuilder.ts b/src/vs/workbench/parts/search/common/queryBuilder.ts index 52392ff3903..861ad5bcd12 100644 --- a/src/vs/workbench/parts/search/common/queryBuilder.ts +++ b/src/vs/workbench/parts/search/common/queryBuilder.ts @@ -11,7 +11,8 @@ import * as collections from 'vs/base/common/collections'; import * as strings from 'vs/base/common/strings'; import * as glob from 'vs/base/common/glob'; import * as paths from 'vs/base/common/paths'; -import uri from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; +import { URI as uri } from 'vs/base/common/uri'; import { untildify } from 'vs/base/common/labels'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IPatternInfo, IQueryOptions, IFolderQuery, ISearchQuery, QueryType, ISearchConfiguration, getExcludes, pathIncludedInQuery } from 'vs/platform/search/common/search'; @@ -73,24 +74,30 @@ export class QueryBuilder { if (contentPattern) { this.resolveSmartCaseToCaseSensitive(contentPattern); + + contentPattern.wordSeparators = this.configurationService.getValue().editor.wordSeparators; } - const query = { + const query: ISearchQuery = { type, folderQueries, usingSearchPaths: !!(searchPaths && searchPaths.length), extraFileResources: options.extraFileResources, - filePattern: options.filePattern, + filePattern: options.filePattern + ? options.filePattern.trim() + : options.filePattern, excludePattern, includePattern, maxResults: options.maxResults, sortByScore: options.sortByScore, cacheKey: options.cacheKey, - contentPattern: contentPattern, + contentPattern, useRipgrep, disregardIgnoreFiles: options.disregardIgnoreFiles || !useIgnoreFiles, disregardExcludeSettings: options.disregardExcludeSettings, - ignoreSymlinks + ignoreSymlinks, + previewOptions: options.previewOptions, + exists: options.exists }; // Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace @@ -270,18 +277,18 @@ export class QueryBuilder { if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) { // TODO: @Sandy Try checking workspace folders length instead. const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri; - return [workspaceUri.with({ path: paths.normalize(paths.join(workspaceUri.path, searchPath)) })]; + return [resources.joinPath(workspaceUri, searchPath)]; } else if (searchPath === './') { return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces } else { const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)([\/\\].+)?/); if (relativeSearchPathMatch) { const searchPathRoot = relativeSearchPathMatch[1]; - const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => paths.basename(folder.uri.fsPath) === searchPathRoot || folder.name === searchPathRoot); + const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => resources.basename(folder.uri) === searchPathRoot || folder.name === searchPathRoot); if (matchingRoots.length) { return matchingRoots.map(root => { return relativeSearchPathMatch[2] ? - root.uri.with({ path: paths.normalize(paths.join(root.uri.path, relativeSearchPathMatch[2])) }) : + resources.joinPath(root.uri, relativeSearchPathMatch[2]) : root.uri; }); } else { diff --git a/src/vs/workbench/parts/search/common/search.ts b/src/vs/workbench/parts/search/common/search.ts index c7eb5fd5b87..2a6b2d6142e 100644 --- a/src/vs/workbench/parts/search/common/search.ts +++ b/src/vs/workbench/parts/search/common/search.ts @@ -9,15 +9,23 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/platform/search/common/search'; -import { SymbolInformation } from 'vs/editor/common/modes'; +import { SymbolKind, Location, ProviderResult } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { toResource } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export interface IWorkspaceSymbol { + name: string; + containerName?: string; + kind: SymbolKind; + location: Location; +} export interface IWorkspaceSymbolProvider { - provideWorkspaceSymbols(search: string): TPromise; - resolveWorkspaceSymbol?: (item: SymbolInformation) => TPromise; + provideWorkspaceSymbols(search: string, token: CancellationToken): ProviderResult; + resolveWorkspaceSymbol?(item: IWorkspaceSymbol, token: CancellationToken): ProviderResult; } export namespace WorkspaceSymbolProviderRegistry { @@ -48,12 +56,12 @@ export namespace WorkspaceSymbolProviderRegistry { } } -export function getWorkspaceSymbols(query: string): TPromise<[IWorkspaceSymbolProvider, SymbolInformation[]][]> { +export function getWorkspaceSymbols(query: string, token: CancellationToken = CancellationToken.None): TPromise<[IWorkspaceSymbolProvider, IWorkspaceSymbol[]][]> { - const result: [IWorkspaceSymbolProvider, SymbolInformation[]][] = []; + const result: [IWorkspaceSymbolProvider, IWorkspaceSymbol[]][] = []; const promises = WorkspaceSymbolProviderRegistry.all().map(support => { - return support.provideWorkspaceSymbols(query).then(value => { + return Promise.resolve(support.provideWorkspaceSymbols(query, token)).then(value => { if (Array.isArray(value)) { result.push([support, value]); } diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index 186d45a23a6..f43b94187fd 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -3,39 +3,51 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as objects from 'vs/base/common/objects'; -import * as strings from 'vs/base/common/strings'; -import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { TPromise, PPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; -import { values, ResourceMap, TernarySearchTree } from 'vs/base/common/map'; -import { Event, Emitter, fromPromise, stopwatch, anyEvent } from 'vs/base/common/event'; -import { ISearchService, ISearchProgressItem, ISearchComplete, ISearchQuery, IPatternInfo, IFileMatch } from 'vs/platform/search/common/search'; -import { ReplacePattern } from 'vs/platform/search/common/replace'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import * as errors from 'vs/base/common/errors'; +import { anyEvent, Emitter, Event, fromPromise, stopwatch } from 'vs/base/common/event'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap, TernarySearchTree, values } from 'vs/base/common/map'; +import * as objects from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, FindMatch } from 'vs/editor/common/model'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; -import { IProgressRunner } from 'vs/platform/progress/common/progress'; +import { FindMatch, IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressRunner } from 'vs/platform/progress/common/progress'; +import { ReplacePattern } from 'vs/platform/search/common/replace'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, TextSearchResult } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { getBaseLabel } from 'vs/base/common/labels'; +import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export class Match { - private _lineText: string; private _id: string; private _range: Range; + private _previewText: string; + private _rangeInPreviewText: Range; - constructor(private _parent: FileMatch, text: string, lineNumber: number, offset: number, length: number) { - this._lineText = text; - this._range = new Range(1 + lineNumber, 1 + offset, 1 + lineNumber, 1 + offset + length); - this._id = this._parent.id() + '>' + lineNumber + '>' + offset + this.getMatchString(); + constructor(private _parent: FileMatch, _result: ITextSearchResult) { + this._range = new Range( + _result.range.startLineNumber + 1, + _result.range.startColumn + 1, + _result.range.endLineNumber + 1, + _result.range.endColumn + 1); + + this._rangeInPreviewText = new Range( + _result.preview.match.startLineNumber + 1, + _result.preview.match.startColumn + 1, + _result.preview.match.endLineNumber + 1, + _result.preview.match.endColumn + 1); + this._previewText = _result.preview.text; + + this._id = this._parent.id() + '>' + this._range + this.getMatchString(); } public id(): string { @@ -47,7 +59,7 @@ export class Match { } public text(): string { - return this._lineText; + return this._previewText; } public range(): Range { @@ -55,11 +67,9 @@ export class Match { } public preview(): { before: string; inside: string; after: string; } { - let before = this._lineText.substring(0, this._range.startColumn - 1), + const before = this._previewText.substring(0, this._rangeInPreviewText.startColumn - 1), inside = this.getMatchString(), - after = this._lineText.substring(this._range.endColumn - 1, Math.min(this._range.endColumn + 150, this._lineText.length)); - - before = strings.lcut(before, 26); + after = this._previewText.substring(this._rangeInPreviewText.endColumn - 1); return { before, @@ -75,7 +85,7 @@ export class Match { // If match string is not matching then regex pattern has a lookahead expression if (replaceString === null) { - replaceString = searchModel.replacePattern.getReplaceString(matchString + this._lineText.substring(this._range.endColumn - 1)); + replaceString = searchModel.replacePattern.getReplaceString(matchString + this._previewText.substring(this._rangeInPreviewText.endColumn - 1)); } // Match string is still not matching. Could be unsupported matches (multi-line). @@ -87,7 +97,7 @@ export class Match { } public getMatchString(): string { - return this._lineText.substring(this._range.startColumn - 1, this._range.endColumn - 1); + return this._previewText.substring(this._rangeInPreviewText.startColumn - 1, this._rangeInPreviewText.endColumn - 1); } } @@ -134,7 +144,7 @@ export class FileMatch extends Disposable { private _updateScheduler: RunOnceScheduler; private _modelDecorations: string[] = []; - constructor(private _query: IPatternInfo, private _maxResults: number, private _parent: FolderMatch, private rawMatch: IFileMatch, + constructor(private _query: IPatternInfo, private _previewOptions: ITextSearchPreviewOptions, private _maxResults: number, private _parent: FolderMatch, private rawMatch: IFileMatch, @IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) { super(); this._resource = this.rawMatch.resource; @@ -152,11 +162,9 @@ export class FileMatch extends Disposable { this.bindModel(model); this.updateMatchesForModel(); } else { - this.rawMatch.lineMatches.forEach((rawLineMatch) => { - rawLineMatch.offsetAndLengths.forEach(offsetAndLength => { - let match = new Match(this, rawLineMatch.preview, rawLineMatch.lineNumber, offsetAndLength[0], offsetAndLength[1]); - this.add(match); - }); + this.rawMatch.matches.forEach((rawLineMatch) => { + let match = new Match(this, rawLineMatch); + this.add(match); }); } } @@ -222,7 +230,9 @@ export class FileMatch extends Disposable { private updateMatches(matches: FindMatch[], modelChange: boolean) { matches.forEach(m => { - let match = new Match(this, this._model.getLineContent(m.range.startLineNumber), m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endColumn - m.range.startColumn); + const textSearchResult = editorMatchToTextSearchResult(m, this._model, this._previewOptions); + const match = new Match(this, textSearchResult); + if (!this._removedMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -348,7 +358,7 @@ export class FolderMatch extends Disposable { private _unDisposedFileMatches: ResourceMap; private _replacingAll: boolean = false; - constructor(private _resource: URI, private _id: string, private _index: number, private _query: ISearchQuery, private _parent: SearchResult, private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, + constructor(private _resource: URI | null, private _id: string, private _index: number, private _query: ISearchQuery, private _parent: SearchResult, private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @IInstantiationService private instantiationService: IInstantiationService) { super(); this._fileMatches = new ResourceMap(); @@ -371,7 +381,7 @@ export class FolderMatch extends Disposable { return this._id; } - public resource(): URI { + public resource(): URI | null { return this._resource; } @@ -387,21 +397,21 @@ export class FolderMatch extends Disposable { return this._parent; } - public hasRoot(): boolean { - return this._resource.fsPath !== ''; + public hasResource(): boolean { + return !!this._resource; } public add(raw: IFileMatch[], silent: boolean): void { - let changed: FileMatch[] = []; + const changed: FileMatch[] = []; raw.forEach((rawFileMatch) => { if (this._fileMatches.has(rawFileMatch.resource)) { this._fileMatches.get(rawFileMatch.resource).dispose(); } - let fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.maxResults, this, rawFileMatch); + const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch); this.doAdd(fileMatch); changed.push(fileMatch); - let disposable = fileMatch.onChange(() => this.onFileChange(fileMatch)); + const disposable = fileMatch.onChange(() => this.onFileChange(fileMatch)); fileMatch.onDispose(() => disposable.dispose()); }); if (!silent && changed.length) { @@ -525,6 +535,7 @@ export class SearchResult extends Disposable { public readonly onChange: Event = this._onChange.event; private _folderMatches: FolderMatch[] = []; + private _otherFilesMatch: FolderMatch; private _folderMatchesMap: TernarySearchTree = TernarySearchTree.forPaths(); private _showHighlights: boolean; @@ -539,17 +550,19 @@ export class SearchResult extends Disposable { public set query(query: ISearchQuery) { // When updating the query we could change the roots, so ensure we clean up the old roots first. this.clear(); - const otherFiles = URI.parse(''); - this._folderMatches = (query.folderQueries || []).map((fq) => fq.folder).concat([otherFiles]).map((resource, index) => { - const id = resource.toString() || 'otherFiles'; - const folderMatch = this.instantiationService.createInstance(FolderMatch, resource, id, index, query, this, this._searchModel); - const disposable = folderMatch.onChange((event) => this._onChange.fire(event)); - folderMatch.onDispose(() => disposable.dispose()); - return folderMatch; - }); - // otherFiles is the fallback for missing values in the TrieMap. So we do not insert it. - this._folderMatches.slice(0, this.folderMatches.length - 1) - .forEach(fm => this._folderMatchesMap.set(fm.resource().fsPath, fm)); + this._folderMatches = (query.folderQueries || []) + .map(fq => fq.folder) + .map((resource, index) => this.createFolderMatch(resource, resource.toString(), index, query)); + this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource().fsPath, fm)); + + this._otherFilesMatch = this.createFolderMatch(null, 'otherFiles', this._folderMatches.length + 1, query); + } + + private createFolderMatch(resource: URI | null, id: string, index: number, query: ISearchQuery): FolderMatch { + const folderMatch = this.instantiationService.createInstance(FolderMatch, resource, id, index, query, this, this._searchModel); + const disposable = folderMatch.onChange((event) => this._onChange.fire(event)); + folderMatch.onDispose(() => disposable.dispose()); + return folderMatch; } public get searchModel(): SearchModel { @@ -558,27 +571,34 @@ export class SearchResult extends Disposable { public add(allRaw: IFileMatch[], silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. - let rawPerFolder = new ResourceMap(); + const rawPerFolder = new ResourceMap(); + const otherFileMatches: IFileMatch[] = []; this._folderMatches.forEach((folderMatch) => rawPerFolder.set(folderMatch.resource(), [])); allRaw.forEach(rawFileMatch => { let folderMatch = this.getFolderMatch(rawFileMatch.resource); - if (folderMatch) { + if (folderMatch.resource()) { rawPerFolder.get(folderMatch.resource()).push(rawFileMatch); + } else { + otherFileMatches.push(rawFileMatch); } }); + rawPerFolder.forEach((raw) => { if (!raw.length) { return; } - let folderMatch = this.getFolderMatch(raw[0].resource); + + const folderMatch = this.getFolderMatch(raw[0].resource); if (folderMatch) { folderMatch.add(raw, silent); } }); + + this.otherFiles.add(otherFileMatches, silent); } public clear(): void { - this._folderMatches.forEach((folderMatch) => folderMatch.clear()); + this.folderMatches().forEach((folderMatch) => folderMatch.clear()); this.disposeMatches(); } @@ -615,19 +635,21 @@ export class SearchResult extends Disposable { } public folderMatches(): FolderMatch[] { - return this._folderMatches.concat(); + return this._otherFilesMatch ? + this._folderMatches.concat(this._otherFilesMatch) : + this._folderMatches.concat(); } public matches(): FileMatch[] { let matches: FileMatch[][] = []; - this._folderMatches.forEach((folderMatch) => { + this.folderMatches().forEach((folderMatch) => { matches.push(folderMatch.matches()); }); return [].concat(...matches); } public isEmpty(): boolean { - return this._folderMatches.every((folderMatch) => folderMatch.isEmpty()); + return this.folderMatches().every((folderMatch) => folderMatch.isEmpty()); } public fileCount(): number { @@ -674,18 +696,19 @@ export class SearchResult extends Disposable { } private get otherFiles(): FolderMatch { - return this._folderMatches[this._folderMatches.length - 1]; + return this._otherFilesMatch; } private set replacingAll(running: boolean) { - this._folderMatches.forEach((folderMatch) => { + this.folderMatches().forEach((folderMatch) => { folderMatch.replacingAll = running; }); } private disposeMatches(): void { - this._folderMatches.forEach(folderMatch => folderMatch.dispose()); + this.folderMatches().forEach(folderMatch => folderMatch.dispose()); this._folderMatches = []; + this._otherFilesMatch = null; this._folderMatchesMap = TernarySearchTree.forPaths(); this._rangeHighlightDecorations.removeHighlightRange(); } @@ -708,7 +731,7 @@ export class SearchModel extends Disposable { private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); public readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; - private currentRequest: PPromise; + private currentCancelTokenSource: CancellationTokenSource; constructor(@ISearchService private searchService: ISearchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) { super(); @@ -743,18 +766,30 @@ export class SearchModel extends Disposable { return this._searchResult; } - public search(query: ISearchQuery): PPromise { + public search(query: ISearchQuery, onProgress?: (result: ISearchProgressItem) => void): TPromise { this.cancelSearch(); + this._searchQuery = query; - this.currentRequest = this.searchService.search(this._searchQuery); - this.searchResult.clear(); - this._searchResult.query = this._searchQuery; + + const progressEmitter = new Emitter(); this._replacePattern = new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); - const onDone = fromPromise(this.currentRequest); - const progressEmitter = new Emitter(); + const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); + const currentRequest = this.searchService.search(this._searchQuery, this.currentCancelTokenSource.token, p => { + progressEmitter.fire(); + this.onSearchProgress(p); + + if (onProgress) { + onProgress(p); + } + }); + + const dispose = () => tokenSource.dispose(); + currentRequest.then(dispose, dispose); + + const onDone = fromPromise(currentRequest); const onFirstRender = anyEvent(onDone, progressEmitter.event); const onFirstRenderStopwatch = stopwatch(onFirstRender); /* __GDPR__ @@ -774,32 +809,34 @@ export class SearchModel extends Disposable { */ onDoneStopwatch(duration => this.telemetryService.publicLog('searchResultsFinished', { duration })); - const currentRequest = this.currentRequest; - this.currentRequest.then( + currentRequest.then( value => this.onSearchCompleted(value, Date.now() - start), - e => this.onSearchError(e, Date.now() - start), - p => { - progressEmitter.fire(); - this.onSearchProgress(p); - } - ); + e => this.onSearchError(e, Date.now() - start)); - // this.currentRequest may be completed (and nulled) immediately return currentRequest; } private onSearchCompleted(completed: ISearchComplete, duration: number): ISearchComplete { - this.currentRequest = null; - const options: IPatternInfo = objects.assign({}, this._searchQuery.contentPattern); delete options.pattern; + + const stats = completed && completed.stats as ITextSearchStats; + + const fileSchemeOnly = this._searchQuery.folderQueries.every(fq => fq.folder.scheme === 'file'); + const otherSchemeOnly = this._searchQuery.folderQueries.every(fq => fq.folder.scheme !== 'file'); + const scheme = fileSchemeOnly ? 'file' : + otherSchemeOnly ? 'other' : + 'mixed'; + /* __GDPR__ "searchResultsShown" : { "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "fileCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "options": { "${inline}": [ "${IPatternInfo}" ] }, "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "useRipgrep": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "useRipgrep": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ this.telemetryService.publicLog('searchResultsShown', { @@ -807,7 +844,9 @@ export class SearchModel extends Disposable { fileCount: this._searchResult.fileCount(), options, duration, - useRipgrep: this._searchQuery.useRipgrep + useRipgrep: this._searchQuery.useRipgrep, + type: stats && stats.type, + scheme }); return completed; } @@ -825,9 +864,8 @@ export class SearchModel extends Disposable { } public cancelSearch(): boolean { - if (this.currentRequest) { - this.currentRequest.cancel(); - this.currentRequest = null; + if (this.currentCancelTokenSource) { + this.currentCancelTokenSource.cancel(); return true; } return false; @@ -945,3 +983,20 @@ export class RangeHighlightDecorations implements IDisposable { isWholeLine: true }); } + +/** + * While search doesn't support multiline matches, collapse editor matches to a single line + */ +export function editorMatchToTextSearchResult(match: FindMatch, model: ITextModel, previewOptions: ITextSearchPreviewOptions): TextSearchResult { + let endLineNumber = match.range.endLineNumber - 1; + let endCol = match.range.endColumn - 1; + if (match.range.endLineNumber !== match.range.startLineNumber) { + endLineNumber = match.range.startLineNumber - 1; + endCol = model.getLineLength(match.range.startLineNumber); + } + + return new TextSearchResult( + model.getLineContent(match.range.startLineNumber), + new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, endLineNumber, endCol), + previewOptions); +} 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 e36535783fb..1b50a6691ce 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -19,7 +19,7 @@ import { ExplorerFolderContext, ExplorerRootContext } from 'vs/workbench/parts/f import { SyncActionDescriptor, MenuRegistry, MenuId, ICommandAction } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions } from 'vs/workbench/browser/quickopen'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -31,7 +31,7 @@ import * as Constants from 'vs/workbench/parts/search/common/constants'; import { registerContributions as replaceContributions } from 'vs/workbench/parts/search/browser/replaceContributions'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/parts/search/browser/searchWidget'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding, ShowPreviousFindTermKeybinding, ShowNextFindTermKeybinding } from 'vs/editor/contrib/find/findModel'; +import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { ISearchWorkbenchService, SearchWorkbenchService } from 'vs/workbench/parts/search/common/searchModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { SearchView } from 'vs/workbench/parts/search/browser/searchView'; @@ -42,7 +42,7 @@ import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { getWorkspaceSymbols } from 'vs/workbench/parts/search/common/search'; import { illegalArgument } from 'vs/base/common/errors'; import { WorkbenchListFocusContextKey, IListService } from 'vs/platform/list/browser/listService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { relative } from 'path'; import { dirname } from 'vs/base/common/resources'; import { ResourceContextKey } from 'vs/workbench/common/resources'; @@ -52,7 +52,7 @@ import { getMultiSelectedResources } from 'vs/workbench/parts/files/browser/file import { Schemas } from 'vs/base/common/network'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, FocusActiveEditorCommand, toggleCaseSensitiveCommand, ShowNextSearchTermAction, ShowPreviousSearchTermAction, toggleRegexCommand, ShowPreviousSearchIncludeAction, ShowNextSearchIncludeAction, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction, copyPathCommand, copyMatchCommand, copyAllCommand, ShowNextSearchExcludeAction, ShowPreviousSearchExcludeAction, clearHistoryCommand, ShowNextReplaceTermAction, ShowPreviousReplaceTermAction } from 'vs/workbench/parts/search/browser/searchActions'; +import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction, ClearSearchResultsAction, copyPathCommand, copyMatchCommand, copyAllCommand, clearHistoryCommand, FocusNextInputAction, FocusPreviousInputAction, RefreshAction, focusSearchListCommand } from 'vs/workbench/parts/search/browser/searchActions'; import { VIEW_ID, ISearchConfigurationProperties } from 'vs/platform/search/common/search'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -68,7 +68,7 @@ const category = nls.localize('search', "Search"); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.search.toggleQueryDetails', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: Constants.SearchViewVisibleKey, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_J, handler: accessor => { @@ -81,9 +81,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.FocusSearchFromResults, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey), - primary: KeyCode.UpArrow, + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); searchView.focusPreviousInputBox(); @@ -92,7 +92,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.OpenMatchToSide, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { @@ -107,7 +107,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CancelActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey), primary: KeyCode.Escape, handler: (accessor, args: any) => { @@ -118,7 +118,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.RemoveActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), primary: KeyCode.Delete, mac: { @@ -133,7 +133,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, handler: (accessor, args: any) => { @@ -145,7 +145,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFileActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], @@ -158,7 +158,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFolderActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], @@ -171,7 +171,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CloseReplaceWidgetActionId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey), primary: KeyCode.Escape, handler: (accessor, args: any) => { @@ -179,6 +179,26 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: FocusNextInputAction.ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + handler: (accessor, args: any) => { + accessor.get(IInstantiationService).createInstance(FocusNextInputAction, FocusNextInputAction.ID, '').run(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: FocusPreviousInputAction.ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated()), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + handler: (accessor, args: any) => { + accessor.get(IInstantiationService).createInstance(FocusPreviousInputAction, FocusPreviousInputAction.ID, '').run(); + } +}); + MenuRegistry.appendMenuItem(MenuId.SearchContext, { command: { id: Constants.ReplaceActionId, @@ -221,7 +241,7 @@ MenuRegistry.appendMenuItem(MenuId.SearchContext, { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CopyMatchCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: Constants.FileMatchOrMatchFocusKey, primary: KeyMod.CtrlCmd | KeyCode.KEY_C, handler: copyMatchCommand @@ -239,7 +259,7 @@ MenuRegistry.appendMenuItem(MenuId.SearchContext, { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CopyPathCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: Constants.FileMatchOrFolderMatchFocusKey, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C, win: { @@ -311,6 +331,19 @@ MenuRegistry.appendMenuItem(MenuId.SearchContext, { order: 1 }); +CommandsRegistry.registerCommand({ + id: Constants.FocusSearchListCommandID, + handler: focusSearchListCommand +}); + +const focusSearchListCommandLabel = nls.localize('focusSearchListCommandLabel', "Focus List"); +const FocusSearchListCommand: ICommandAction = { + id: Constants.FocusSearchListCommandID, + title: focusSearchListCommandLabel, + category +}; +MenuRegistry.addCommand(FocusSearchListCommand); + const FIND_IN_FOLDER_ID = 'filesExplorer.findInFolder'; CommandsRegistry.registerCommand({ id: FIND_IN_FOLDER_ID, @@ -348,6 +381,13 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: RefreshAction.ID, + handler: (accessor, args: any) => { + accessor.get(IInstantiationService).createInstance(RefreshAction, RefreshAction.ID, '').run(); + } +}); + const FIND_IN_WORKSPACE_ID = 'filesExplorer.findInWorkspace'; CommandsRegistry.registerCommand({ id: FIND_IN_WORKSPACE_ID, @@ -432,62 +472,59 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Actions const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, VIEW_ID, nls.localize('showSearchViewl', "Show Search"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, - Constants.SearchViewVisibleKey.toNegated()), 'View: Show Search', nls.localize('view', "View")); -registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, Constants.FindInFilesActionId, nls.localize('findInFiles', "Find in Files"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, - Constants.SearchInputBoxFocusedKey.toNegated()), 'Find in Files', category); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.FocusActiveEditorCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey), - handler: FocusActiveEditorCommand, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F +// Show Search and Find in Files are redundant, but we can't break keybindings by removing one. So it's the same action, same keybinding, registered to different IDs. +// Show Search 'when' is redundant but if the two conflict with exactly the same keybinding and 'when' clause, then they can show up as "unbound" - #51780 +registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, VIEW_ID, nls.localize('showSearchViewl', "Show Search"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, Constants.SearchViewVisibleKey.toNegated()), 'View: Show Search', nls.localize('view', "View")); +registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, Constants.FindInFilesActionId, nls.localize('findInFiles', "Find in Files"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }), 'Find in Files', category); +MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '4_find_global', + command: { + id: Constants.FindInFilesActionId, + title: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files") + }, + order: 1 }); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Next Search Result', category); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Previous Search Result', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ReplaceInFilesAction, ReplaceInFilesAction.ID, ReplaceInFilesAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_H }), 'Replace in Files', category); +MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '4_find_global', + command: { + id: ReplaceInFilesAction.ID, + title: nls.localize({ key: 'miReplaceInFiles', comment: ['&& denotes a mnemonic'] }, "Replace &&in Files") + }, + order: 2 +}); KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ id: Constants.ToggleCaseSensitiveCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey), handler: toggleCaseSensitiveCommand }, ToggleCaseSensitiveKeybinding)); KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ id: Constants.ToggleWholeWordCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey), handler: toggleWholeWordCommand }, ToggleWholeWordKeybinding)); KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ id: Constants.ToggleRegexCommandId, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey), handler: toggleRegexCommand }, ToggleRegexKeybinding)); -// Terms navigation actions -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchTermAction, ShowNextSearchTermAction.ID, ShowNextSearchTermAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey)), 'Search: Show Next Search Term', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchTermAction, ShowPreviousSearchTermAction.ID, ShowPreviousSearchTermAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.SearchInputBoxFocusedKey)), 'Search: Show Previous Search Term', category); - -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextReplaceTermAction, ShowNextReplaceTermAction.ID, ShowNextReplaceTermAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey)), 'Search: Show Next Search Replace Term', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousReplaceTermAction, ShowPreviousReplaceTermAction.ID, ShowPreviousReplaceTermAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey)), 'Search: Show Previous Search Replace Term', category); - -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchIncludeAction, ShowNextSearchIncludeAction.ID, ShowNextSearchIncludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Next Search Include Pattern', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchIncludeAction, ShowPreviousSearchIncludeAction.ID, ShowPreviousSearchIncludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Previous Search Include Pattern', category); - -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchExcludeAction, ShowNextSearchExcludeAction.ID, ShowNextSearchExcludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Next Search Exclude Pattern', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchExcludeAction, ShowPreviousSearchExcludeAction.ID, ShowPreviousSearchExcludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Previous Search Exclude Pattern', category); - registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL), 'Search: Collapse All', category); - registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllSymbolsAction, ShowAllSymbolsAction.ID, ShowAllSymbolsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_T }), 'Go to Symbol in Workspace...'); +registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshAction, RefreshAction.ID, RefreshAction.LABEL), 'Search: Refresh', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL), 'Search: Clear', category); + // Register Quick Open Handler Registry.as(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler( @@ -526,7 +563,7 @@ configurationRegistry.registerConfiguration({ properties: { 'search.exclude': { type: 'object', - description: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting."), + markdownDescription: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), default: { '**/node_modules': true, '**/bower_components': true }, additionalProperties: { anyOf: [ @@ -551,18 +588,18 @@ configurationRegistry.registerConfiguration({ }, 'search.useRipgrep': { type: 'boolean', - description: nls.localize('useRipgrep', "Controls whether to use ripgrep in text and file search"), + description: nls.localize('useRipgrep', "Controls whether to use ripgrep in text and file search."), default: true }, 'search.useIgnoreFiles': { type: 'boolean', - description: nls.localize('useIgnoreFiles', "Controls whether to use .gitignore and .ignore files when searching for files."), + markdownDescription: nls.localize('useIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files when searching for files."), default: true, scope: ConfigurationScope.RESOURCE }, 'search.quickOpen.includeSymbols': { type: 'boolean', - description: nls.localize('search.quickOpen.includeSymbols', "Configure to include results from a global symbol search in the file results for Quick Open."), + description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, 'search.followSymlinks': { @@ -572,20 +609,31 @@ configurationRegistry.registerConfiguration({ }, 'search.smartCase': { type: 'boolean', - description: nls.localize('search.smartCase', "Searches case-insensitively if the pattern is all lowercase, otherwise, searches case-sensitively"), + description: nls.localize('search.smartCase', "Search case-insensitively if the pattern is all lowercase, otherwise, search case-sensitively."), default: false }, 'search.globalFindClipboard': { type: 'boolean', default: false, - description: nls.localize('search.globalFindClipboard', "Controls if the search view should read or modify the shared find clipboard on macOS"), + description: nls.localize('search.globalFindClipboard', "Controls whether the search view should read or modify the shared find clipboard on macOS."), included: platform.isMacintosh }, 'search.location': { type: 'string', enum: ['sidebar', 'panel'], default: 'sidebar', - description: nls.localize('search.location', "Controls if the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space."), + description: nls.localize('search.location', "Controls whether the search will be shown as a view in the sidebar or as a panel in the panel area for more horizontal space."), + }, + 'search.collapseResults': { + type: 'string', + enum: ['auto', 'alwaysCollapse', 'alwaysExpand'], + enumDescriptions: [ + 'Files with less than 10 results are expanded. Others are collapsed.', + '', + '' + ], + default: 'auto', + description: nls.localize('search.collapseAllResults', "Controls whether the search results will be collapsed or expanded."), } } }); @@ -597,3 +645,25 @@ registerLanguageCommand('_executeWorkspaceSymbolProvider', function (accessor, a } return getWorkspaceSymbols(query); }); + +// View menu + +MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_views', + command: { + id: VIEW_ID, + title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search") + }, + order: 2 +}); + +// Go to menu + +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.showAllSymbols', + title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") + }, + order: 3 +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/search/test/browser/searchActions.test.ts b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts index 47f5db6be49..c71828c4b40 100644 --- a/src/vs/workbench/parts/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TestInstantiationService, stubFunction } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel'; import { ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions'; @@ -129,13 +129,26 @@ suite('Search Actions', () => { function aFileMatch(): FileMatch { let rawMatch: IFileMatch = { resource: URI.file('somepath' + ++counter), - lineMatches: [] + matches: [] }; - return instantiationService.createInstance(FileMatch, null, null, null, rawMatch); + return instantiationService.createInstance(FileMatch, null, null, null, null, rawMatch); } function aMatch(fileMatch: FileMatch): Match { - let match = new Match(fileMatch, 'some match', ++counter, 0, 2); + const line = ++counter; + const range = { + startLineNumber: line, + startColumn: 0, + endLineNumber: line, + endColumn: 2 + }; + let match = new Match(fileMatch, { + preview: { + text: 'some match', + match: range + }, + range + }); fileMatch.add(match); return match; } 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 2c273d2744a..798d4fb6c92 100644 --- a/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts @@ -5,11 +5,11 @@ 'use strict'; import * as assert from 'assert'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { Match, FileMatch, SearchResult } from 'vs/workbench/parts/search/common/searchModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { SearchDataSource, SearchSorter } from 'vs/workbench/parts/search/browser/searchResultsView'; -import { IFileMatch, ILineMatch } from 'vs/platform/search/common/search'; +import { IFileMatch, TextSearchResult, OneLineRange, ITextSearchResult } from 'vs/platform/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -31,9 +31,22 @@ suite('Search - Viewlet', () => { let ds = instantiation.createInstance(SearchDataSource); let result: SearchResult = instantiation.createInstance(SearchResult, null); result.query = { type: 1, folderQueries: [{ folder: uri.parse('file://c:/') }] }; + + const range = { + startLineNumber: 1, + startColumn: 0, + endLineNumber: 1, + endColumn: 1 + }; result.add([{ resource: uri.parse('file:///c:/foo'), - lineMatches: [{ lineNumber: 1, preview: 'bar', offsetAndLengths: [[0, 1]] }] + matches: [{ + preview: { + text: 'bar', + match: range + }, + range + }] }]); let fileMatch = result.matches()[0]; @@ -41,7 +54,7 @@ suite('Search - Viewlet', () => { assert.equal(ds.getId(null, result), 'root'); assert.equal(ds.getId(null, fileMatch), 'file:///c%3A/foo'); - assert.equal(ds.getId(null, lineMatch), 'file:///c%3A/foo>1>0b'); + assert.equal(ds.getId(null, lineMatch), 'file:///c%3A/foo>[2,1 -> 2,2]b'); assert(!ds.hasChildren(null, 'foo')); assert(ds.hasChildren(null, result)); @@ -53,9 +66,9 @@ suite('Search - Viewlet', () => { let fileMatch1 = aFileMatch('C:\\foo'); let fileMatch2 = aFileMatch('C:\\with\\path'); let fileMatch3 = aFileMatch('C:\\with\\path\\foo'); - let lineMatch1 = new Match(fileMatch1, 'bar', 1, 1, 1); - let lineMatch2 = new Match(fileMatch1, 'bar', 2, 1, 1); - let lineMatch3 = new Match(fileMatch1, 'bar', 2, 1, 1); + let lineMatch1 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(0, 1, 1))); + let lineMatch2 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(2, 1, 1))); + let lineMatch3 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(2, 1, 1))); let s = new SearchSorter(); @@ -69,16 +82,16 @@ suite('Search - Viewlet', () => { assert(s.compare(null, lineMatch2, lineMatch3) === 0); }); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch { + function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchResult[]): FileMatch { let rawMatch: IFileMatch = { resource: uri.file('C:\\' + path), - lineMatches: lineMatches + matches: lineMatches }; - return instantiation.createInstance(FileMatch, null, null, searchResult, rawMatch); + return instantiation.createInstance(FileMatch, null, null, null, searchResult, rawMatch); } function stubModelService(instantiationService: TestInstantiationService): IModelService { instantiationService.stub(IConfigurationService, new TestConfigurationService()); return instantiationService.createInstance(ModelServiceImpl); } -}); \ No newline at end of file +}); 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 c11c6994b96..efdacfc6d06 100644 --- a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IExpression } from 'vs/base/common/glob'; import * as paths from 'vs/base/common/paths'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -17,6 +17,7 @@ import { IWorkspaceContextService, toWorkspaceFolders, Workspace } from 'vs/plat import { ISearchPathsResult, QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder'; import { TestContextService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true }; const DEFAULT_QUERY_PROPS = { useRipgrep: true, disregardIgnoreFiles: false }; @@ -36,10 +37,11 @@ suite('QueryBuilder', () => { mockConfigService = new TestConfigurationService(); mockConfigService.setUserConfiguration('search', DEFAULT_USER_CONFIG); + mockConfigService.setUserConfiguration('editor', DEFAULT_EDITOR_CONFIG); instantiationService.stub(IConfigurationService, mockConfigService); mockContextService = new TestContextService(); - mockWorkspace = new Workspace('workspace', 'workspace', toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }])); + mockWorkspace = new Workspace('workspace', toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }])); mockContextService.setWorkspace(mockWorkspace); instantiationService.stub(IWorkspaceContextService, mockContextService); @@ -233,6 +235,21 @@ suite('QueryBuilder', () => { }); }); + test('file pattern trimming', () => { + const content = 'content'; + assertEqualQueries( + queryBuilder.text( + PATTERN_INFO, + undefined, + { filePattern: ` ${content} ` } + ), + { + contentPattern: PATTERN_INFO, + filePattern: content, + type: QueryType.Text + }); + }); + test('exclude ./ syntax', () => { assertEqualQueries( queryBuilder.text( 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 ae9d4349c90..3e7e4f5ca12 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -6,20 +6,21 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { DeferredPPromise } from 'vs/base/test/common/utils'; -import { PPromise } from 'vs/base/common/winjs.base'; -import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; -import URI from 'vs/base/common/uri'; -import { IFileMatch, IFolderQuery, ILineMatch, ISearchService, ISearchComplete, ISearchProgressItem, IUncachedSearchStats } from 'vs/platform/search/common/search'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { DeferredTPromise } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { timeout } from 'vs/base/common/async'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchResult, TextSearchResult, OneLineRange } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; const nullEvent = new class { @@ -41,21 +42,25 @@ const nullEvent = new class { } }; +const lineOneRange = new OneLineRange(1, 0, 1); suite('SearchModel', () => { let instantiationService: TestInstantiationService; let restoreStubs: sinon.SinonStub[]; - const testSearchStats: IUncachedSearchStats = { + const testSearchStats: IFileSearchStats = { fromCache: false, - resultCount: 4, - traversal: 'node', - errors: [], - fileWalkStartTime: 0, - fileWalkResultTime: 1, - directoriesWalked: 2, - filesWalked: 3 + resultCount: 1, + type: 'searchProcess', + detailStats: { + traversal: 'node', + fileWalkTime: 0, + cmdTime: 0, + cmdResultCount: 0, + directoriesWalked: 2, + filesWalked: 3 + } }; const folderQueries: IFolderQuery[] = [ @@ -68,7 +73,7 @@ suite('SearchModel', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModelService, stubModelService(instantiationService)); instantiationService.stub(ISearchService, {}); - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: [] })); + instantiationService.stub(ISearchService, 'search', TPromise.as({ results: [] })); }); teardown(() => { @@ -77,18 +82,52 @@ suite('SearchModel', () => { }); }); - function ppromiseWithProgress(results: IFileMatch[]): () => PPromise { - return () => new PPromise((resolve, reject, progress) => { - process.nextTick(() => { - results.forEach(progress); - resolve(null); - }); - }); + function searchServiceWithResults(results: IFileMatch[], complete: ISearchComplete = null): ISearchService { + return { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { + return new TPromise(resolve => { + process.nextTick(() => { + results.forEach(onProgress); + resolve(complete); + }); + }); + } + }; + } + + function searchServiceWithError(error: Error): ISearchService { + return { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { + return new TPromise((resolve, reject) => { + reject(error); + }); + } + }; + } + + function canceleableSearchService(tokenSource: CancellationTokenSource): ISearchService { + return { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { + if (token) { + token.onCancellationRequested(() => tokenSource.cancel()); + } + + return new TPromise(resolve => { + process.nextTick(() => { + resolve({}); + }); + }); + } + }; } test('Search Model: Search adds to results', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', new TextSearchResult('preview 2', lineOneRange))]; + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -113,10 +152,15 @@ suite('SearchModel', () => { test('Search Model: Search reports telemetry on search completed', async () => { let target = instantiationService.spy(ITelemetryService, 'publicLog'); - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; + instantiationService.stub(ISearchService, searchServiceWithResults(results)); - let testObject = instantiationService.createInstance(SearchModel); + let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(target.calledThrice); @@ -131,7 +175,7 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress([])); + instantiationService.stub(ISearchService, searchServiceWithResults([])); let testObject = instantiationService.createInstance(SearchModel); const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -150,17 +194,17 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); - instantiationService.stub(ISearchService, 'search', promise); + instantiationService.stub(ISearchService, searchServiceWithResults( + [aRawMatch('file://c:/1', new TextSearchResult('some preview', lineOneRange))], + { results: [], stats: testSearchStats })); let testObject = instantiationService.createInstance(SearchModel); let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - promise.progress(aRawMatch('file://c:/1', aLineMatch('some preview'))); - promise.complete({ results: [], stats: testSearchStats }); - - return timeout(1).then(() => { - return result.then(() => { + return result.then(() => { + return timeout(1).then(() => { + // timeout because promise handlers may run in a different order. We only care that these + // are fired at some point. assert.ok(target1.calledWith('searchResultsFirstRender')); assert.ok(target1.calledWith('searchResultsFinished')); // assert.equal(1, target2.callCount); @@ -174,14 +218,11 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); - instantiationService.stub(ISearchService, 'search', promise); + instantiationService.stub(ISearchService, searchServiceWithError(new Error('error'))); let testObject = instantiationService.createInstance(SearchModel); let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - promise.error('error'); - return timeout(1).then(() => { return result.then(() => { }, () => { assert.ok(target1.calledWith('searchResultsFirstRender')); @@ -197,7 +238,7 @@ suite('SearchModel', () => { let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - let promise = new DeferredPPromise(); + let promise = new DeferredTPromise(); instantiationService.stub(ISearchService, 'search', promise); let testObject = instantiationService.createInstance(SearchModel); @@ -215,33 +256,41 @@ suite('SearchModel', () => { }); test('Search Model: Search results are cleared during search', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(!testObject.searchResult.isEmpty()); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise()); + instantiationService.stub(ISearchService, searchServiceWithResults([])); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(testObject.searchResult.isEmpty()); }); test('Search Model: Previous search is cancelled when new search is called', async () => { - let target = sinon.spy(); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise((c, e, p) => { }, target)); - let testObject: SearchModel = instantiationService.createInstance(SearchModel); + const tokenSource = new CancellationTokenSource(); + instantiationService.stub(ISearchService, canceleableSearchService(tokenSource)); + const testObject: SearchModel = instantiationService.createInstance(SearchModel); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - instantiationService.stub(ISearchService, 'search', new DeferredPPromise()); + instantiationService.stub(ISearchService, searchServiceWithResults([])); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - assert.ok(target.calledOnce); + assert.ok(tokenSource.token.isCancellationRequested); }); test('getReplaceString returns proper replace string for regExpressions', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]))]; - instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11)))]; + instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries }); @@ -267,12 +316,8 @@ suite('SearchModel', () => { assert.equal('helloe', match.replaceString); }); - function aRawMatch(resource: string, ...lineMatches: ILineMatch[]): IFileMatch { - return { resource: URI.parse(resource), lineMatches }; - } - - function aLineMatch(preview: string, lineNumber: number = 1, offsetAndLengths: number[][] = [[0, 1]]): ILineMatch { - return { preview, lineNumber, offsetAndLengths }; + function aRawMatch(resource: string, ...matches: ITextSearchResult[]): IFileMatch { + return { resource: URI.parse(resource), matches }; } function stub(arg1: any, arg2: any, arg3: any): sinon.SinonStub { 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 e9f486bc193..a001b34e061 100644 --- a/src/vs/workbench/parts/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchResult.test.ts @@ -8,8 +8,8 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Match, FileMatch, SearchResult, SearchModel } from 'vs/workbench/parts/search/common/searchModel'; -import URI from 'vs/base/common/uri'; -import { IFileMatch, ILineMatch } from 'vs/platform/search/common/search'; +import { URI } from 'vs/base/common/uri'; +import { IFileMatch, TextSearchResult, OneLineRange, ITextSearchResult } from 'vs/platform/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { Range } from 'vs/editor/common/core/range'; @@ -19,6 +19,8 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; +const lineOneRange = new OneLineRange(1, 0, 1); + suite('SearchResult', () => { let instantiationService: TestInstantiationService; @@ -33,21 +35,17 @@ suite('SearchResult', () => { test('Line Match', function () { let fileMatch = aFileMatch('folder/file.txt', null); - let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3); + let lineMatch = new Match(fileMatch, new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); assert.equal(lineMatch.text(), 'foo bar'); assert.equal(lineMatch.range().startLineNumber, 2); assert.equal(lineMatch.range().endLineNumber, 2); assert.equal(lineMatch.range().startColumn, 1); assert.equal(lineMatch.range().endColumn, 4); - assert.equal('file:///folder/file.txt>1>0foo', lineMatch.id()); + assert.equal('file:///folder/file.txt>[2,1 -> 2,4]foo', lineMatch.id()); }); test('Line Match - Remove', function () { - let fileMatch = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo bar', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }]); + let fileMatch = aFileMatch('folder/file.txt', aSearchResult(), new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); let lineMatch = fileMatch.matches()[0]; fileMatch.remove(lineMatch); assert.equal(fileMatch.matches().length, 0); @@ -66,15 +64,11 @@ suite('SearchResult', () => { }); test('File Match: Select an existing match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); @@ -82,15 +76,11 @@ suite('SearchResult', () => { }); test('File Match: Select non existing match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); let target = testObject.matches()[0]; testObject.remove(target); @@ -100,15 +90,11 @@ suite('SearchResult', () => { }); test('File Match: isSelected return true for selected match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); let target = testObject.matches()[0]; testObject.setSelectedMatch(target); @@ -116,32 +102,20 @@ suite('SearchResult', () => { }); test('File Match: isSelected return false for un-selected match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch('folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); - assert.ok(!testObject.isMatchSelected(testObject.matches()[1])); }); test('File Match: unselect', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); testObject.setSelectedMatch(null); @@ -149,16 +123,11 @@ suite('SearchResult', () => { }); test('File Match: unselect when not selected', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(null); assert.equal(null, testObject.getSelectedMatch()); @@ -167,7 +136,7 @@ suite('SearchResult', () => { test('Alle Drei Zusammen', function () { let searchResult = instantiationService.createInstance(SearchResult, null); let fileMatch = aFileMatch('far/boo', searchResult); - let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3); + let lineMatch = new Match(fileMatch, new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult); @@ -175,7 +144,10 @@ suite('SearchResult', () => { test('Adding a raw match will add a file match with line matches', function () { let testObject = aSearchResult(); - let target = [aRawMatch('file://c:/', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]), aLineMatch('preview 2'))]; + let target = [aRawMatch('file://c:/', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11)), + new TextSearchResult('preview 2', lineOneRange))]; testObject.add(target); @@ -200,7 +172,12 @@ suite('SearchResult', () => { test('Adding multiple raw matches', function () { let testObject = aSearchResult(); - let target = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; + let target = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; testObject.add(target); @@ -228,7 +205,11 @@ suite('SearchResult', () => { let target2 = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1')), aRawMatch('file://c:/2', aLineMatch('preview 2'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange)), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]); testObject.matches()[0].onDispose(target1); testObject.matches()[1].onDispose(target2); @@ -243,7 +224,9 @@ suite('SearchResult', () => { test('remove triggers change event', function () { let target = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let objectRoRemove = testObject.matches()[0]; testObject.onChange(target); @@ -256,7 +239,9 @@ suite('SearchResult', () => { test('remove triggers change event', function () { let target = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let objectRoRemove = testObject.matches()[0]; testObject.onChange(target); @@ -268,7 +253,9 @@ suite('SearchResult', () => { test('Removing all line matches and adding back will add file back to result', function () { let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let target = testObject.matches()[0]; let matchToRemove = target.matches()[0]; target.remove(matchToRemove); @@ -283,7 +270,9 @@ suite('SearchResult', () => { test('replace should remove the file match', function () { instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); testObject.replace(testObject.matches()[0]); @@ -294,7 +283,9 @@ suite('SearchResult', () => { let target = sinon.spy(); instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); testObject.onChange(target); let objectRoRemove = testObject.matches()[0]; @@ -307,7 +298,11 @@ suite('SearchResult', () => { test('replaceAll should remove all file matches', function () { instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1')), aRawMatch('file://c:/2', aLineMatch('preview 2'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange)), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]); testObject.replaceAll(null); @@ -358,12 +353,12 @@ suite('SearchResult', () => { // lineHasNoDecoration(oneModel, 2); //}); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch { + function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchResult[]): FileMatch { let rawMatch: IFileMatch = { resource: URI.file('/' + path), - lineMatches: lineMatches + matches: lineMatches }; - return instantiationService.createInstance(FileMatch, null, null, searchResult, rawMatch); + return instantiationService.createInstance(FileMatch, null, null, null, searchResult, rawMatch); } function aSearchResult(): SearchResult { @@ -372,12 +367,8 @@ suite('SearchResult', () => { return searchModel.searchResult; } - function aRawMatch(resource: string, ...lineMatches: ILineMatch[]): IFileMatch { - return { resource: URI.parse(resource), lineMatches }; - } - - function aLineMatch(preview: string, lineNumber: number = 1, offsetAndLengths: number[][] = [[0, 1]]): ILineMatch { - return { preview, lineNumber, offsetAndLengths }; + function aRawMatch(resource: string, ...matches: ITextSearchResult[]): IFileMatch { + return { resource: URI.parse(resource), matches }; } function stubModelService(instantiationService: TestInstantiationService): IModelService { diff --git a/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts index 46cacc7d9aa..61dcf49479a 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/configureSnippets.ts @@ -9,15 +9,15 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { writeFile, exists } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { join, basename, dirname, extname } from 'path'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { timeout } from 'vs/base/common/async'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; import { values } from 'vs/base/common/map'; +import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; const id = 'workbench.action.openSnippets'; @@ -27,7 +27,7 @@ namespace ISnippetPick { } } -interface ISnippetPick extends IPickOpenEntry { +interface ISnippetPick extends IQuickPickItem { filepath: string; hint?: true; } @@ -65,8 +65,8 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } existing.push({ - label: basename(file.filepath), - filepath: file.filepath, + label: basename(file.location.fsPath), + filepath: file.location.fsPath, description: names.size === 0 ? nls.localize('global.scope', "(global)") : nls.localize('global.1', "({0})", values(names).join(', ')) @@ -74,11 +74,11 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } else { // language snippet - const mode = basename(file.filepath, '.json'); + const mode = basename(file.location.fsPath, '.json'); existing.push({ - label: basename(file.filepath), + label: basename(file.location.fsPath), description: `(${modeService.getLanguageName(mode)})`, - filepath: file.filepath + filepath: file.location.fsPath }); seen.add(mode); } @@ -179,22 +179,23 @@ async function createLanguageSnippetFile(pick: ISnippetPick) { CommandsRegistry.registerCommand(id, async accessor => { const snippetService = accessor.get(ISnippetsService); - const quickOpenService = accessor.get(IQuickOpenService); + const quickInputService = accessor.get(IQuickInputService); const opener = accessor.get(IOpenerService); const windowService = accessor.get(IWindowService); const modeService = accessor.get(IModeService); const envService = accessor.get(IEnvironmentService); - const { existing, future } = await computePicks(snippetService, envService, modeService); - const newGlobalPick = { label: nls.localize('new.global', "New Global Snippets file...") }; + const picks = await computePicks(snippetService, envService, modeService); + const existing: QuickPickInput[] = picks.existing; + const newGlobalPick = { label: nls.localize('new.global', "New Global Snippets file...") }; if (existing.length > 0) { - existing[0].separator = { label: nls.localize('group.global', "Existing Snippets") }; - newGlobalPick.separator = { border: true, label: nls.localize('new.global.sep', "New Snippets") }; + existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); } else { - newGlobalPick.separator = { label: nls.localize('new.global.sep', "New Snippets") }; + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); } - const pick = await quickOpenService.pick(<(IPickOpenEntry | ISnippetPick)[]>[].concat(existing, newGlobalPick, future), { + const pick = await quickInputService.pick(<(IQuickPickItem | ISnippetPick)[]>[].concat(existing, newGlobalPick, picks.future), { placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), matchOnDescription: true }); @@ -217,3 +218,12 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { category: nls.localize('preferences', "Preferences") } }); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '3_snippets', + command: { + id, + title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") + }, + order: 1 +}); diff --git a/src/vs/workbench/parts/snippets/electron-browser/insertSnippet.ts b/src/vs/workbench/parts/snippets/electron-browser/insertSnippet.ts index 581e4854c2f..57b29e05e18 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/insertSnippet.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/insertSnippet.ts @@ -5,9 +5,7 @@ 'use strict'; import * as nls from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { IModeService } from 'vs/editor/common/services/modeService'; import { LanguageId } from 'vs/editor/common/modes'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -16,8 +14,9 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2 import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { IQuickPickItem, IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -interface ISnippetPick extends IPickOpenEntry { +interface ISnippetPick extends IQuickPickItem { snippet: Snippet; } @@ -63,7 +62,7 @@ class InsertSnippetAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor, arg: any): TPromise { + public run(accessor: ServicesAccessor, editor: ICodeEditor, arg: any): Promise { const modeService = accessor.get(IModeService); const snippetService = accessor.get(ISnippetsService); @@ -71,11 +70,11 @@ class InsertSnippetAction extends EditorAction { return undefined; } - const quickOpenService = accessor.get(IQuickOpenService); + const quickInputService = accessor.get(IQuickInputService); const { lineNumber, column } = editor.getPosition(); let { snippet, name, langId } = Args.fromUser(arg); - return new TPromise(async (resolve, reject) => { + return new Promise(async (resolve, reject) => { if (snippet) { return resolve(new Snippet( @@ -116,7 +115,7 @@ class InsertSnippetAction extends EditorAction { } else { // let user pick a snippet const snippets = (await snippetService.getSnippets(languageId)).sort(Snippet.compare); - const picks: ISnippetPick[] = []; + const picks: QuickPickInput[] = []; let prevSnippet: Snippet; for (const snippet of snippets) { const pick: ISnippetPick = { @@ -125,14 +124,14 @@ class InsertSnippetAction extends EditorAction { snippet }; if (!snippet.isFromExtension && !prevSnippet) { - pick.separator = { label: nls.localize('sep.userSnippet', "User Snippets") }; + picks.push({ type: 'separator', label: nls.localize('sep.userSnippet', "User Snippets") }); } else if (snippet.isFromExtension && (!prevSnippet || !prevSnippet.isFromExtension)) { - pick.separator = { label: nls.localize('sep.extSnippet', "Extension Snippets") }; + picks.push({ type: 'separator', label: nls.localize('sep.extSnippet', "Extension Snippets") }); } picks.push(pick); prevSnippet = snippet; } - return quickOpenService.pick(picks, { matchOnDetail: true }).then(pick => resolve(pick && pick.snippet), reject); + return quickInputService.pick(picks, { matchOnDetail: true }).then(pick => resolve(pick && pick.snippet), reject); } }).then(snippet => { if (snippet) { diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts index ad5dd186af1..a22a19668a2 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsFile.ts @@ -9,17 +9,20 @@ import { parse as jsonParse } from 'vs/base/common/json'; import { forEach } from 'vs/base/common/collections'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { localize } from 'vs/nls'; -import { readFile } from 'vs/base/node/pfs'; import { basename, extname } from 'path'; import { SnippetParser, Variable, Placeholder, Text } from 'vs/editor/contrib/snippet/snippetParser'; import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/snippetVariables'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; export class Snippet { private _codeSnippet: string; private _isBogous: boolean; + readonly prefixLow: string; + constructor( readonly scopes: string[], readonly name: string, @@ -30,6 +33,7 @@ export class Snippet { readonly isFromExtension?: boolean, ) { // + this.prefixLow = prefix ? prefix.toLowerCase() : prefix; } get codeSnippet(): string { @@ -140,11 +144,12 @@ export class SnippetFile { private _loadPromise: Promise; constructor( - readonly filepath: string, + readonly location: URI, readonly defaultScopes: string[], - private readonly _extension: IExtensionDescription + private readonly _extension: IExtensionDescription, + private readonly _fileService: IFileService ) { - this.isGlobalSnippets = extname(filepath) === '.code-snippets'; + this.isGlobalSnippets = extname(location.path) === '.code-snippets'; this.isUserSnippets = !this._extension; } @@ -158,7 +163,7 @@ export class SnippetFile { private _filepathSelect(selector: string, bucket: Snippet[]): void { // for `fooLang.json` files all snippets are accepted - if (selector === basename(this.filepath, '.json')) { + if (selector === basename(this.location.path, '.json')) { bucket.push(...this.data); } } @@ -190,8 +195,8 @@ export class SnippetFile { load(): Promise { if (!this._loadPromise) { - this._loadPromise = Promise.resolve(readFile(this.filepath)).then(value => { - const data = jsonParse(value.toString()); + this._loadPromise = Promise.resolve(this._fileService.resolveContent(this.location, { encoding: 'utf8' })).then(content => { + const data = jsonParse(content.value.toString()); if (typeof data === 'object') { forEach(data, entry => { const { key: name, value: scopeOrTemplate } = entry; diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts index 7222ae708a2..d7e52f8df9b 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsService.ts @@ -4,30 +4,33 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { localize } from 'vs/nls'; -import { ITextModel } from 'vs/editor/common/model'; -import { ISuggestSupport, ISuggestResult, ISuggestion, LanguageId, SuggestionType, SnippetType } from 'vs/editor/common/modes'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { Position } from 'vs/editor/common/core/position'; -import { overlap, compare, startsWith, isFalsyOrWhitespace, endsWith } from 'vs/base/common/strings'; -import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { join, basename, extname } from 'path'; -import { mkdirp, readdir, exists } from 'vs/base/node/pfs'; -import { watch } from 'vs/base/node/extfs'; -import { SnippetFile, Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; -import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; +import { basename, extname, join } from 'path'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { values } from 'vs/base/common/map'; +import * as resources from 'vs/base/common/resources'; +import { compare, endsWith, isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { watch } from 'vs/base/node/extfs'; +import { exists, mkdirp, readdir } from 'vs/base/node/pfs'; +import { Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { ISuggestion, ISuggestResult, ISuggestSupport, LanguageId, SnippetType, SuggestContext, SuggestionType } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { values } from 'vs/base/common/map'; +import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; +import { Snippet, SnippetFile } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; namespace schema { @@ -36,7 +39,12 @@ namespace schema { path: string; } - export function isValidSnippet(extension: IExtensionPointUser, snippet: ISnippetsExtensionPoint, modeService: IModeService): boolean { + export interface IValidSnippetsExtensionPoint { + language: string; + location: URI; + } + + export function toValidSnippet(extension: IExtensionPointUser, snippet: ISnippetsExtensionPoint, modeService: IModeService): IValidSnippetsExtensionPoint { if (isFalsyOrWhitespace(snippet.path)) { extension.collector.error(localize( @@ -44,37 +52,43 @@ namespace schema { "Expected string in `contributes.{0}.path`. Provided value: {1}", extension.description.name, String(snippet.path) )); - return false; - } else if (isFalsyOrWhitespace(snippet.language) && !endsWith(snippet.path, '.code-snippets')) { + return null; + } + + if (isFalsyOrWhitespace(snippet.language) && !endsWith(snippet.path, '.code-snippets')) { extension.collector.error(localize( 'invalid.language.0', "When omitting the language, the value of `contributes.{0}.path` must be a `.code-snippets`-file. Provided value: {1}", extension.description.name, String(snippet.path) )); - return false; - } else if (!isFalsyOrWhitespace(snippet.language) && !modeService.isRegisteredMode(snippet.language)) { + return null; + } + + if (!isFalsyOrWhitespace(snippet.language) && !modeService.isRegisteredMode(snippet.language)) { extension.collector.error(localize( 'invalid.language', "Unknown language in `contributes.{0}.language`. Provided value: {1}", extension.description.name, String(snippet.language) )); - return false; + return null; - } else { - // TODO@extensionLocation - const normalizedAbsolutePath = join(extension.description.extensionLocation.fsPath, snippet.path); - if (normalizedAbsolutePath.indexOf(extension.description.extensionLocation.fsPath) !== 0) { - extension.collector.error(localize( - 'invalid.path.1', - "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", - extension.description.name, normalizedAbsolutePath, extension.description.extensionLocation.fsPath - )); - return false; - } - - snippet.path = normalizedAbsolutePath; - return true; } + + const extensionLocation = extension.description.extensionLocation; + const snippetLocation = resources.joinPath(extensionLocation, snippet.path); + if (!resources.isEqualOrParent(snippetLocation, extensionLocation)) { + extension.collector.error(localize( + 'invalid.path.1', + "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", + extension.description.name, snippetLocation.path, extensionLocation.path + )); + return null; + } + + return { + language: snippet.language, + location: snippetLocation + }; } export const snippetsContribution: IJSONSchema = { @@ -111,7 +125,8 @@ class SnippetsService implements ISnippetsService { @IModeService private readonly _modeService: IModeService, @ILogService private readonly _logService: ILogService, @IExtensionService extensionService: IExtensionService, - @ILifecycleService lifecycleService: ILifecycleService + @ILifecycleService lifecycleService: ILifecycleService, + @IFileService private readonly _fileService: IFileService, ) { this._initExtensionSnippets(); this._initPromise = Promise.resolve(lifecycleService.when(LifecyclePhase.Running).then(() => this._initUserSnippets())); @@ -135,7 +150,7 @@ class SnippetsService implements ISnippetsService { this._files.forEach(file => { promises.push(file.load() .then(file => file.select(langName, result)) - .catch(err => this._logService.error(err, file.filepath)) + .catch(err => this._logService.error(err, file.location.toString())) ); }); return Promise.all(promises).then(() => result); @@ -160,16 +175,17 @@ class SnippetsService implements ISnippetsService { ExtensionsRegistry.registerExtensionPoint('snippets', [languagesExtPoint], schema.snippetsContribution).setHandler(extensions => { for (const extension of extensions) { for (const contribution of extension.value) { - if (!schema.isValidSnippet(extension, contribution, this._modeService)) { + const validContribution = schema.toValidSnippet(extension, contribution, this._modeService); + if (!validContribution) { continue; } - if (this._files.has(contribution.path)) { - this._files.get(contribution.path).defaultScopes.push(contribution.language); + if (this._files.has(validContribution.location.toString())) { + this._files.get(validContribution.location.toString()).defaultScopes.push(validContribution.language); } else { - const file = new SnippetFile(contribution.path, contribution.language ? [contribution.language] : undefined, extension.description); - this._files.set(file.filepath, file); + const file = new SnippetFile(validContribution.location, validContribution.language ? [validContribution.language] : undefined, extension.description, this._fileService); + this._files.set(file.location.toString(), file); if (this._environmentService.isExtensionDevelopment) { file.load().then(file => { @@ -186,7 +202,7 @@ class SnippetsService implements ISnippetsService { extension.collector.warn(localize( 'badFile', "The snippet file \"{0}\" could not be read.", - file.filepath + file.location.toString() )); }); } @@ -202,10 +218,10 @@ class SnippetsService implements ISnippetsService { const ext = extname(filepath); if (ext === '.json') { const langName = basename(filepath, '.json'); - this._files.set(filepath, new SnippetFile(filepath, [langName], undefined)); + this._files.set(filepath, new SnippetFile(URI.file(filepath), [langName], undefined, this._fileService)); } else if (ext === '.code-snippets') { - this._files.set(filepath, new SnippetFile(filepath, undefined, undefined)); + this._files.set(filepath, new SnippetFile(URI.file(filepath), undefined, undefined, this._fileService)); } }; @@ -218,7 +234,7 @@ class SnippetsService implements ISnippetsService { } }).then(() => { // watch - const watcher = watch(userSnippetsFolder, (type, filename) => { + this._disposables.push(watch(userSnippetsFolder, (type, filename) => { if (typeof filename !== 'string') { return; } @@ -236,15 +252,7 @@ class SnippetsService implements ISnippetsService { this._files.delete(filepath); } }); - }, (error: string) => this._logService.error(error)); - this._disposables.push({ - dispose: () => { - if (watcher) { - watcher.removeAllListeners(); - watcher.close(); - } - } - }); + }, (error: string) => this._logService.error(error))); }).then(undefined, err => { this._logService.error('Failed to load user snippets', err); @@ -305,35 +313,51 @@ export class SnippetSuggestProvider implements ISuggestSupport { // } - provideCompletionItems(model: ITextModel, position: Position): Promise { + provideCompletionItems(model: ITextModel, position: Position, context: SuggestContext): Promise { const languageId = this._getLanguageIdAtPosition(model, position); return this._snippets.getSnippets(languageId).then(snippets => { - const suggestions: SnippetSuggestion[] = []; + let suggestions: SnippetSuggestion[]; + let pos = { lineNumber: position.lineNumber, column: Math.max(1, position.column - 100) }; + let lineOffsets: number[] = []; + let linePrefixLow = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); - const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase(); - const lowLineUntil = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase(); + while (pos.column < position.column) { + let word = model.getWordAtPosition(pos); + if (word) { + // at a word + lineOffsets.push(word.startColumn - 1); + pos.column = word.endColumn + 1; - for (const snippet of snippets) { + if (word.endColumn - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) { + lineOffsets.push(word.endColumn - 1); + } - const lowPrefix = snippet.prefix.toLowerCase(); - let overwriteBefore = 0; - let accetSnippet = true; - - if (lowWordUntil.length > 0 && startsWith(lowPrefix, lowWordUntil)) { - // cheap match on the (none-empty) current word - overwriteBefore = lowWordUntil.length; - accetSnippet = true; - - } else if (lowLineUntil.length > 0 && lowLineUntil.match(/[^\s]$/)) { - // compute overlap between snippet and (none-empty) line on text - overwriteBefore = overlap(lowLineUntil, snippet.prefix.toLowerCase()); - accetSnippet = overwriteBefore > 0 && !model.getWordAtPosition(new Position(position.lineNumber, position.column - overwriteBefore)); + } else if (!/\s/.test(linePrefixLow[pos.column - 1])) { + // at a none-whitespace character + lineOffsets.push(pos.column - 1); + pos.column += 1; + } else { + // always advance! + pos.column += 1; } + } - if (accetSnippet) { - suggestions.push(new SnippetSuggestion(snippet, overwriteBefore)); + if (lineOffsets.length === 0) { + // no interesting spans found -> pick all snippets + suggestions = snippets.map(snippet => new SnippetSuggestion(snippet, 0)); + + } else { + let consumed = new Set(); + suggestions = []; + for (const start of lineOffsets) { + for (const snippet of snippets) { + if (!consumed.has(snippet) && matches(linePrefixLow, start, snippet.prefixLow, 0)) { + suggestions.push(new SnippetSuggestion(snippet, linePrefixLow.length - start)); + consumed.add(snippet); + } + } } } @@ -373,6 +397,16 @@ export class SnippetSuggestProvider implements ISuggestSupport { } } +function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean { + while (patternStart < pattern.length && wordStart < word.length) { + if (pattern[patternStart] === word[wordStart]) { + patternStart += 1; + } + wordStart += 1; + } + return patternStart === pattern.length; +} + export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): string { /** * Do not analyze more characters diff --git a/src/vs/workbench/parts/snippets/electron-browser/tabCompletion.ts b/src/vs/workbench/parts/snippets/electron-browser/tabCompletion.ts index 9b2b047160c..93c0e82a043 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/tabCompletion.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/tabCompletion.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { KeyCode } from 'vs/base/common/keyCodes'; import { RawContextKey, IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; import { getNonWhitespacePrefix, SnippetSuggestion } from 'vs/workbench/parts/snippets/electron-browser/snippetsService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -142,7 +142,7 @@ registerEditorCommand(new TabCompletionCommand({ precondition: TabCompletionController.ContextKey, handler: x => x.performSnippetCompletions(), kbOpts: { - weight: KeybindingsRegistry.WEIGHT.editorContrib(), + weight: KeybindingWeight.EditorContrib, kbExpr: ContextKeyExpr.and( EditorContextKeys.editorTextFocus, EditorContextKeys.tabDoesNotMoveFocus, diff --git a/src/vs/workbench/parts/snippets/test/electron-browser/snippetFile.test.ts b/src/vs/workbench/parts/snippets/test/electron-browser/snippetFile.test.ts index 00b8b9adbe6..5f31b802ae8 100644 --- a/src/vs/workbench/parts/snippets/test/electron-browser/snippetFile.test.ts +++ b/src/vs/workbench/parts/snippets/test/electron-browser/snippetFile.test.ts @@ -7,23 +7,24 @@ import * as assert from 'assert'; import { SnippetFile, Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { URI } from 'vs/base/common/uri'; suite('Snippets', function () { class TestSnippetFile extends SnippetFile { - constructor(filepath: string, snippets: Snippet[]) { - super(filepath, undefined, undefined); + constructor(filepath: URI, snippets: Snippet[]) { + super(filepath, undefined, undefined, null); this.data.push(...snippets); } } test('SnippetFile#select', function () { - let file = new TestSnippetFile('somepath/foo.code-snippets', []); + let file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), []); let bucket: Snippet[] = []; file.select('', bucket); assert.equal(bucket.length, 0); - file = new TestSnippetFile('somepath/foo.code-snippets', [ + file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test'), new Snippet(['foo'], 'FooSnippet2', 'foo', '', 'snippet', 'test'), new Snippet(['bar'], 'BarSnippet1', 'foo', '', 'snippet', 'test'), @@ -55,7 +56,7 @@ suite('Snippets', function () { test('SnippetFile#select - any scope', function () { - let file = new TestSnippetFile('somepath/foo.code-snippets', [ + let file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ new Snippet([], 'AnySnippet1', 'foo', '', 'snippet', 'test'), new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test'), ]); diff --git a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts index cd3123b3b6a..70f0ab60b99 100644 --- a/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts +++ b/src/vs/workbench/parts/snippets/test/electron-browser/snippetsService.test.ts @@ -13,6 +13,7 @@ import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution'; import { Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile'; +import { SuggestContext, SuggestTriggerKind } from 'vs/editor/common/modes'; class SimpleSnippetService implements ISnippetsService { _serviceBrand: any; @@ -40,6 +41,7 @@ suite('SnippetsService', function () { let modeService: ModeServiceImpl; let snippetService: ISnippetsService; + let suggestContext: SuggestContext = { triggerKind: SuggestTriggerKind.Invoke }; setup(function () { modeService = new ModeServiceImpl(); @@ -66,7 +68,7 @@ suite('SnippetsService', function () { const provider = new SnippetSuggestProvider(modeService, snippetService); const model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang')); - return provider.provideCompletionItems(model, new Position(1, 1)).then(result => { + return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => { assert.equal(result.incomplete, undefined); assert.equal(result.suggestions.length, 2); }); @@ -77,14 +79,67 @@ suite('SnippetsService', function () { const provider = new SnippetSuggestProvider(modeService, snippetService); const model = TextModel.createFromString('bar', undefined, modeService.getLanguageIdentifier('fooLang')); - return provider.provideCompletionItems(model, new Position(1, 4)).then(result => { + return provider.provideCompletionItems(model, new Position(1, 4), suggestContext).then(result => { assert.equal(result.incomplete, undefined); assert.equal(result.suggestions.length, 1); assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].overwriteBefore, 3); assert.equal(result.suggestions[0].insertText, 'barCodeSnippet'); }); }); + test('snippet completions - with different prefixes', async function () { + + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'barTest', + 'bar', + '', + 's1', + '' + ), new Snippet( + ['fooLang'], + 'name', + 'bar-bar', + '', + 's2', + '' + )]); + + const provider = new SnippetSuggestProvider(modeService, snippetService); + const model = TextModel.createFromString('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); + + await provider.provideCompletionItems(model, new Position(1, 3), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 2); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 2); + }); + + await provider.provideCompletionItems(model, new Position(1, 5), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].label, 'bar-bar'); + assert.equal(result.suggestions[0].insertText, 's2'); + assert.equal(result.suggestions[0].overwriteBefore, 4); + }); + + await provider.provideCompletionItems(model, new Position(1, 6), suggestContext).then(result => { + assert.equal(result.incomplete, undefined); + assert.equal(result.suggestions.length, 2); + assert.equal(result.suggestions[0].label, 'bar'); + assert.equal(result.suggestions[0].insertText, 's1'); + assert.equal(result.suggestions[0].overwriteBefore, 1); + assert.equal(result.suggestions[1].label, 'bar-bar'); + assert.equal(result.suggestions[1].insertText, 's2'); + assert.equal(result.suggestions[1].overwriteBefore, 5); + }); + }); + test('Cannot use " { + return provider.provideCompletionItems(model, new Position(1, 7), suggestContext).then(result => { assert.equal(result.suggestions.length, 1); model.dispose(); model = TextModel.createFromString('\t { assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); model = TextModel.createFromString('a { - - assert.equal(result.suggestions.length, 0); + assert.equal(result.suggestions.length, 1); + assert.equal(result.suggestions[0].overwriteBefore, 2); model.dispose(); }); }); @@ -131,9 +187,9 @@ suite('SnippetsService', function () { const provider = new SnippetSuggestProvider(modeService, snippetService); let model = TextModel.createFromString('\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang')); - return provider.provideCompletionItems(model, new Position(1, 1)).then(result => { + return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => { assert.equal(result.suggestions.length, 1); - return provider.provideCompletionItems(model, new Position(2, 2)); + return provider.provideCompletionItems(model, new Position(2, 2), suggestContext); }).then(result => { assert.equal(result.suggestions.length, 1); }); @@ -161,11 +217,34 @@ suite('SnippetsService', function () { const provider = new SnippetSuggestProvider(modeService, snippetService); let model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang')); - return provider.provideCompletionItems(model, new Position(1, 1)).then(result => { + return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => { assert.equal(result.suggestions.length, 2); let [first, second] = result.suggestions; assert.equal(first.label, 'first'); assert.equal(second.label, 'second'); }); }); + + test('Dash in snippets prefix broken #53945', async function () { + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'p-a', + 'p-a', + '', + 'second', + '' + )]); + const provider = new SnippetSuggestProvider(modeService, snippetService); + + let model = TextModel.createFromString('p-', undefined, modeService.getLanguageIdentifier('fooLang')); + + let result = await provider.provideCompletionItems(model, new Position(1, 2), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), suggestContext); + assert.equal(result.suggestions.length, 1); + + result = await provider.provideCompletionItems(model, new Position(1, 3), { triggerCharacter: '-', triggerKind: SuggestTriggerKind.TriggerCharacter }); + assert.equal(result.suggestions.length, 1); + }); }); diff --git a/src/vs/workbench/parts/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/parts/splash/electron-browser/partsSplash.contribution.ts new file mode 100644 index 00000000000..5f76b802ea5 --- /dev/null +++ b/src/vs/workbench/parts/splash/electron-browser/partsSplash.contribution.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { getTotalHeight, getTotalWidth } from 'vs/base/browser/dom'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IThemeService, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; +import { IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService'; +import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import * as themes from 'vs/workbench/common/theme'; +import { IPartService, Parts, Position } from 'vs/workbench/services/part/common/partService'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { debounceEvent } from 'vs/base/common/event'; +import { DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; +import { ColorIdentifier, editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { Color } from 'vs/base/common/color'; + +class PartsSplash { + + private static readonly _splashElementId = 'monaco-parts-splash'; + + private readonly _disposables: IDisposable[] = []; + + private lastBaseTheme: string; + private lastBackground: string; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IPartService private readonly _partService: IPartService, + @IStorageService private readonly _storageService: IStorageService, + @ILifecycleService lifecycleService: ILifecycleService, + @IBroadcastService private broadcastService: IBroadcastService + ) { + lifecycleService.when(LifecyclePhase.Running).then(_ => this._removePartsSplash()); + debounceEvent(_partService.onEditorLayout, () => { }, 50)(this._savePartsSplash, this, this._disposables); + } + + dispose(): void { + dispose(this._disposables); + } + + private _savePartsSplash() { + const baseTheme = getThemeTypeSelector(this._themeService.getTheme().type); + const colorInfo = { + foreground: this._getThemeColor(foreground), + editorBackground: this._getThemeColor(editorBackground), + titleBarBackground: this._getThemeColor(themes.TITLE_BAR_ACTIVE_BACKGROUND), + activityBarBackground: this._getThemeColor(themes.ACTIVITY_BAR_BACKGROUND), + sideBarBackground: this._getThemeColor(themes.SIDE_BAR_BACKGROUND), + statusBarBackground: this._getThemeColor(themes.STATUS_BAR_BACKGROUND), + statusBarNoFolderBackground: this._getThemeColor(themes.STATUS_BAR_NO_FOLDER_BACKGROUND), + }; + const layoutInfo = { + sideBarSide: this._partService.getSideBarPosition() === Position.RIGHT ? 'right' : 'left', + editorPartMinWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, + titleBarHeight: getTotalHeight(this._partService.getContainer(Parts.TITLEBAR_PART)), + activityBarWidth: getTotalWidth(this._partService.getContainer(Parts.ACTIVITYBAR_PART)), + sideBarWidth: getTotalWidth(this._partService.getContainer(Parts.SIDEBAR_PART)), + statusBarHeight: getTotalHeight(this._partService.getContainer(Parts.STATUSBAR_PART)), + }; + this._storageService.store('parts-splash-data', JSON.stringify({ id: PartsSplash._splashElementId, colorInfo, layoutInfo, baseTheme }), StorageScope.GLOBAL); + + if (baseTheme !== this.lastBaseTheme || colorInfo.editorBackground !== this.lastBackground) { + // notify the main window on background color changes: the main window sets the background color to new windows + this.lastBaseTheme = baseTheme; + this.lastBackground = colorInfo.editorBackground; + + // the color needs to be in hex + const backgroundColor = this._themeService.getTheme().getColor(editorBackground) || themes.WORKBENCH_BACKGROUND(this._themeService.getTheme()); + this.broadcastService.broadcast({ channel: 'vscode:changeColorTheme', payload: JSON.stringify({ baseTheme, background: Color.Format.CSS.formatHex(backgroundColor) }) }); + } + } + + private _getThemeColor(id: ColorIdentifier): string { + const theme = this._themeService.getTheme(); + const color = theme.getColor(id); + return color ? color.toString() : undefined; + } + + private _removePartsSplash(): void { + let element = document.getElementById(PartsSplash._splashElementId); + if (element) { + element.remove(); + } + } +} + +Registry.as(Extensions.Workbench).registerWorkbenchContribution(PartsSplash, LifecyclePhase.Starting); diff --git a/src/vs/workbench/parts/stats/node/workspaceStats.ts b/src/vs/workbench/parts/stats/node/workspaceStats.ts index 3eec8a04f22..ac0c3d9451a 100644 --- a/src/vs/workbench/parts/stats/node/workspaceStats.ts +++ b/src/vs/workbench/parts/stats/node/workspaceStats.ts @@ -8,7 +8,7 @@ import * as crypto from 'crypto'; import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IFileService, IFileStat, IResolveFileResult } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -16,6 +16,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { endsWith } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/; const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/; @@ -179,6 +180,8 @@ export class WorkspaceStats implements IWorkbenchContribution { this.reportCloudStats(); } + public static tags: Tags; + private searchArray(arr: string[], regEx: RegExp): boolean { return arr.some(v => v.search(regEx) > -1) || undefined; } @@ -226,7 +229,7 @@ export class WorkspaceStats implements IWorkbenchContribution { "workspace.reactNative" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ - private async getWorkspaceTags(configuration: IWindowConfiguration): TPromise { + private getWorkspaceTags(configuration: IWindowConfiguration): TPromise { const tags: Tags = Object.create(null); const state = this.contextService.getWorkbenchState(); @@ -238,7 +241,8 @@ export class WorkspaceStats implements IWorkbenchContribution { workspaceId = void 0; break; case WorkbenchState.FOLDER: - workspaceId = crypto.createHash('sha1').update(workspace.folders[0].uri.fsPath).digest('hex'); + // TODO: #54483 @Ben + workspaceId = crypto.createHash('sha1').update(workspace.folders[0].uri.scheme === Schemas.file ? workspace.folders[0].uri.fsPath : workspace.folders[0].uri.toString()).digest('hex'); break; case WorkbenchState.WORKSPACE: workspaceId = crypto.createHash('sha1').update(workspace.configuration.fsPath).digest('hex'); @@ -256,9 +260,11 @@ export class WorkspaceStats implements IWorkbenchContribution { tags['workspace.empty'] = isEmpty; const folders = !isEmpty ? workspace.folders.map(folder => folder.uri) : this.environmentService.appQuality !== 'stable' && this.findFolders(configuration); - if (folders && folders.length && this.fileService) { - //return - const files: IResolveFileResult[] = await this.fileService.resolveFiles(folders.map(resource => ({ resource }))); + if (!folders || !folders.length || !this.fileService) { + return TPromise.as(tags); + } + + return this.fileService.resolveFiles(folders.map(resource => ({ resource }))).then((files: IResolveFileResult[]) => { const names = ([]).concat(...files.map(result => result.success ? (result.stat.children || []) : [])).map(c => c.name); const nameSet = names.reduce((s, n) => s.add(n.toLowerCase()), new Set()); @@ -311,33 +317,36 @@ export class WorkspaceStats implements IWorkbenchContribution { tags['workspace.android.cpp'] = true; } - if (nameSet.has('package.json')) { - await TPromise.join(folders.map(async workspaceUri => { - const uri = workspaceUri.with({ path: `${workspaceUri.path !== '/' ? workspaceUri.path : ''}/package.json` }); - try { - const content = await this.fileService.resolveContent(uri, { acceptTextOnly: true }); - const packageJsonContents = JSON.parse(content.value); - if (packageJsonContents['dependencies']) { - for (let module of ModulesToLookFor) { - if ('react-native' === module) { - if (packageJsonContents['dependencies'][module]) { - tags['workspace.reactNative'] = true; - } - } else { - if (packageJsonContents['dependencies'][module]) { - tags['workspace.npm.' + module] = true; + const packageJsonPromises = !nameSet.has('package.json') ? [] : folders.map(workspaceUri => { + const uri = workspaceUri.with({ path: `${workspaceUri.path !== '/' ? workspaceUri.path : ''}/package.json` }); + return this.fileService.resolveFile(uri).then(() => { + return this.fileService.resolveContent(uri, { acceptTextOnly: true }).then(content => { + try { + const packageJsonContents = JSON.parse(content.value); + if (packageJsonContents['dependencies']) { + for (let module of ModulesToLookFor) { + if ('react-native' === module) { + if (packageJsonContents['dependencies'][module]) { + tags['workspace.reactNative'] = true; + } + } else { + if (packageJsonContents['dependencies'][module]) { + tags['workspace.npm.' + module] = true; + } } } } } - } - catch (e) { - // Ignore errors when resolving file or parsing file contents - } - })); - } - } - return TPromise.as(tags); + catch (e) { + // Ignore errors when resolving file or parsing file contents + } + }); + }, err => { + // Ignore missing file + }); + }); + return TPromise.join(packageJsonPromises).then(() => tags); + }); } private findFolders(configuration: IWindowConfiguration): URI[] { @@ -347,11 +356,11 @@ export class WorkspaceStats implements IWorkbenchContribution { private findFolder({ filesToOpen, filesToCreate, filesToDiff }: IWindowConfiguration): URI { if (filesToOpen && filesToOpen.length) { - return this.parentURI(URI.file(filesToOpen[0].filePath)); + return this.parentURI(filesToOpen[0].fileUri); } else if (filesToCreate && filesToCreate.length) { - return this.parentURI(URI.file(filesToCreate[0].filePath)); + return this.parentURI(filesToCreate[0].fileUri); } else if (filesToDiff && filesToDiff.length) { - return this.parentURI(URI.file(filesToDiff[0].filePath)); + return this.parentURI(filesToDiff[0].fileUri); } return undefined; } @@ -372,6 +381,7 @@ export class WorkspaceStats implements IWorkbenchContribution { } */ this.telemetryService.publicLog('workspce.tags', tags); + WorkspaceStats.tags = tags; }, error => onUnexpectedError(error)); } diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index 18c84f752e2..b9502e6d967 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -18,6 +18,7 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { Task, CustomTask, ContributedTask } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, RunOptions } from 'vs/workbench/parts/tasks/common/taskService'; import { ActionBarContributor, ContributableActionProvider } from 'vs/workbench/browser/actions'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class TaskEntry extends Model.QuickOpenEntry { @@ -68,7 +69,6 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { private tasks: TPromise<(CustomTask | ContributedTask)[]>; - constructor( protected quickOpenService: IQuickOpenService, protected taskService: ITaskService @@ -87,10 +87,10 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { this.tasks = undefined; } - public getResults(input: string): TPromise { + public getResults(input: string, token: CancellationToken): TPromise { return this.tasks.then((tasks) => { let entries: Model.QuickOpenEntry[] = []; - if (tasks.length === 0) { + if (tasks.length === 0 || token.isCancellationRequested) { return new Model.QuickOpenModel(entries); } let recentlyUsedTasks = this.taskService.getRecentlyUsedTasks(); diff --git a/src/vs/workbench/parts/tasks/common/problemCollectors.ts b/src/vs/workbench/parts/tasks/common/problemCollectors.ts index bcc398f8d40..ea3c90be3c5 100644 --- a/src/vs/workbench/parts/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/parts/tasks/common/problemCollectors.ts @@ -5,7 +5,7 @@ 'use strict'; import { IStringDictionary, INumberDictionary } from 'vs/base/common/collections'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -15,7 +15,7 @@ import { ILineMatcher, createLineMatcher, ProblemMatcher, ProblemMatch, ApplyToK import { IMarkerService, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { generateUuid } from 'vs/base/common/uuid'; -export enum ProblemCollectorEventKind { +export const enum ProblemCollectorEventKind { BackgroundProcessingBegins = 'backgroundProcessingBegins', BackgroundProcessingEnds = 'backgroundProcessingEnds' } @@ -325,7 +325,7 @@ export class AbstractProblemCollector implements IDisposable { } } -export enum ProblemHandlingStrategy { +export const enum ProblemHandlingStrategy { Clean } diff --git a/src/vs/workbench/parts/tasks/common/problemMatcher.ts b/src/vs/workbench/parts/tasks/common/problemMatcher.ts index b2b792cfe02..c253081e8ad 100644 --- a/src/vs/workbench/parts/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/parts/tasks/common/problemMatcher.ts @@ -14,7 +14,7 @@ import * as Types from 'vs/base/common/types'; import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ValidationStatus, ValidationState, IProblemReporter, Parser } from 'vs/base/common/parsers'; @@ -134,7 +134,7 @@ export interface ProblemMatcher { pattern: ProblemPattern | ProblemPattern[]; severity?: Severity; watching?: WatchingMatcher; - fileSystemScheme?: string; + uriProvider?: (path: string) => URI; } export interface NamedProblemMatcher extends ProblemMatcher { @@ -196,8 +196,8 @@ export function getResource(filename: string, matcher: ProblemMatcher): URI { if (fullPath[0] !== '/') { fullPath = '/' + fullPath; } - if (matcher.fileSystemScheme !== void 0) { - return URI.parse(`${matcher.fileSystemScheme}://${fullPath}`); + if (matcher.uriProvider !== void 0) { + return matcher.uriProvider(fullPath); } else { return URI.file(fullPath); } @@ -1088,7 +1088,7 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { } resolve(undefined); }); - }, () => { }); + }); } public onReady(): TPromise { @@ -1642,7 +1642,7 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { } resolve(undefined); }); - }, () => { }); + }); } public onReady(): TPromise { diff --git a/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts index 7a2bab4ba3a..0fdf9cb5134 100644 --- a/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/parts/tasks/common/taskDefinitionRegistry.ts @@ -64,7 +64,7 @@ namespace Configuration { } } } - return { extensionId, taskType, required: required.length >= 0 ? required : undefined, properties: value.properties ? Objects.deepClone(value.properties) : undefined }; + return { extensionId, taskType, required: required, properties: value.properties ? Objects.deepClone(value.properties) : {} }; } } @@ -80,12 +80,14 @@ export interface ITaskDefinitionRegistry { get(key: string): Tasks.TaskDefinition; all(): Tasks.TaskDefinition[]; + getJsonSchema(): IJSONSchema; } class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { private taskTypes: IStringDictionary; private readyPromise: TPromise; + private _schema: IJSONSchema; constructor() { this.taskTypes = Object.create(null); @@ -105,7 +107,7 @@ class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { } resolve(undefined); }); - }, () => { }); + }); } public onReady(): TPromise { @@ -119,6 +121,33 @@ class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { public all(): Tasks.TaskDefinition[] { return Object.keys(this.taskTypes).map(key => this.taskTypes[key]); } + + public getJsonSchema(): IJSONSchema { + if (this._schema === void 0) { + let schemas: IJSONSchema[] = []; + for (let definition of this.all()) { + let schema: IJSONSchema = { + type: 'object', + additionalProperties: false + }; + if (definition.required.length > 0) { + schema.required = definition.required.slice(0); + } + if (definition.properties !== void 0) { + schema.properties = Objects.deepClone(definition.properties); + } else { + schema.properties = Object.create(null); + } + schema.properties.type = { + type: 'string', + enum: [definition.taskType] + }; + schemas.push(schema); + } + this._schema = { oneOf: schemas }; + } + return this._schema; + } } export const TaskDefinitionRegistry: ITaskDefinitionRegistry = new TaskDefinitionRegistryImpl(); diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index b148fa0f5e7..50e7dadcfe5 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -11,15 +11,16 @@ import { LinkedMap } from 'vs/base/common/map'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { Task, ContributedTask, CustomTask, TaskSet, TaskSorter, TaskEvent } from 'vs/workbench/parts/tasks/common/tasks'; +import { Task, ContributedTask, CustomTask, TaskSet, TaskSorter, TaskEvent, TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSummary, TaskTerminateResponse, TaskSystemInfo } from 'vs/workbench/parts/tasks/common/taskSystem'; +import { IStringDictionary } from 'vs/base/common/collections'; export { ITaskSummary, Task, TaskTerminateResponse }; export const ITaskService = createDecorator('taskService'); export interface ITaskProvider { - provideTasks(): TPromise; + provideTasks(validTypes: IStringDictionary): TPromise; } export interface RunOptions { @@ -56,7 +57,7 @@ export interface ITaskService { /** * @param alias The task's name, label or defined identifier. */ - getTask(workspaceFolder: IWorkspaceFolder | string, alias: string, compareId?: boolean): TPromise; + getTask(workspaceFolder: IWorkspaceFolder | string, alias: string | TaskIdentifier, compareId?: boolean): TPromise; getTasksForGroup(group: string): TPromise; getRecentlyUsedTasks(): LinkedMap; createSorter(): TaskSorter; @@ -70,4 +71,4 @@ export interface ITaskService { unregisterTaskProvider(handle: number): boolean; registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/tasks/common/taskSystem.ts b/src/vs/workbench/parts/tasks/common/taskSystem.ts index 38407b78949..0194024af55 100644 --- a/src/vs/workbench/parts/tasks/common/taskSystem.ts +++ b/src/vs/workbench/parts/tasks/common/taskSystem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { URI } from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { TerminateResponse } from 'vs/base/common/processes'; @@ -14,7 +15,7 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Task, TaskEvent, KeyedTaskIdentifier } from './tasks'; -export enum TaskErrors { +export const enum TaskErrors { NotConfigured, RunningTask, NoBuildTask, @@ -77,7 +78,7 @@ export interface ITaskSummary { exitCode?: number; } -export enum TaskExecuteKind { +export const enum TaskExecuteKind { Started = 1, Active = 2 } @@ -103,9 +104,9 @@ export interface TaskTerminateResponse extends TerminateResponse { } export interface TaskSystemInfo { - fileSystemScheme: string; platform: Platform; context: any; + uriProvider: (this: void, path: string) => URI; resolveVariables(workspaceFolder: IWorkspaceFolder, variables: Set): TPromise>; } diff --git a/src/vs/workbench/parts/tasks/common/taskTemplates.ts b/src/vs/workbench/parts/tasks/common/taskTemplates.ts index e19cfd42648..77998b88ac6 100644 --- a/src/vs/workbench/parts/tasks/common/taskTemplates.ts +++ b/src/vs/workbench/parts/tasks/common/taskTemplates.ts @@ -6,9 +6,9 @@ import * as nls from 'vs/nls'; -import { IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; +import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -export interface TaskEntry extends IPickOpenEntry { +export interface TaskEntry extends IQuickPickItem { sort?: string; autoDetect: boolean; content: string; diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index f7157c77f53..5818ba2b4bc 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -12,6 +12,9 @@ import { UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ProblemMatcher } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); export enum ShellQuoting { /** @@ -72,7 +75,7 @@ export interface ShellConfiguration { /** * The shell executable. */ - executable: string; + executable?: string; /** * The arguments to be passed to the shell executable. @@ -294,7 +297,7 @@ export namespace TaskGroup { export type TaskGroup = 'clean' | 'build' | 'rebuild' | 'test'; -export enum TaskScope { +export const enum TaskScope { Global = 1, Workspace = 2, Folder = 3 @@ -354,7 +357,7 @@ export interface TaskDependency { task: string | KeyedTaskIdentifier; } -export enum GroupType { +export const enum GroupType { default = 'default', user = 'user' } @@ -671,7 +674,7 @@ export namespace ExecutionEngine { export const _default: ExecutionEngine = ExecutionEngine.Terminal; } -export enum JsonSchemaVersion { +export const enum JsonSchemaVersion { V0_1_0 = 1, V2_0_0 = 2 } @@ -721,7 +724,7 @@ export class TaskSorter { } } -export enum TaskEventKind { +export const enum TaskEventKind { Start = 'start', ProcessStarted = 'processStarted', Active = 'active', @@ -733,7 +736,7 @@ export enum TaskEventKind { } -export enum TaskRunType { +export const enum TaskRunType { SingleRun = 'singleRun', Background = 'background' } diff --git a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v1.ts b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v1.ts index 4ba603fe91e..8fc1c97a78e 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v1.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v1.ts @@ -23,6 +23,7 @@ const schema: IJSONSchema = { version: { type: 'string', enum: ['0.1.0'], + deprecationMessage: nls.localize('JsonSchema.version.deprecated', 'Task version 0.1.0 is deprecated. Please use 2.0.0'), description: nls.localize('JsonSchema.version', 'The config\'s version number') }, _runner: { 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 b365e527b6e..210e94870ec 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -271,7 +271,8 @@ const version: IJSONSchema = { const identifier: IJSONSchema = { type: 'string', - description: nls.localize('JsonSchema.tasks.identifier', 'A user defined identifier to reference the task in launch.json or a dependsOn clause.') + description: nls.localize('JsonSchema.tasks.identifier', 'A user defined identifier to reference the task in launch.json or a dependsOn clause.'), + deprecationMessage: nls.localize('JsonSchema.tasks.identifier.deprecated', 'User defined identifiers are deprecated. For custom task used the name as a reference and for tasks provided by extensions use their defined task identifier.') }; const options: IJSONSchema = Objects.deepClone(commonSchema.definitions.options); @@ -324,9 +325,11 @@ TaskDefinitionRegistry.onReady().then(() => { if (taskType.required) { schema.required = taskType.required.slice(); } - for (let key of Object.keys(taskType.properties)) { - let property = taskType.properties[key]; - schema.properties[key] = Objects.deepClone(property); + if (taskType.properties) { + for (let key of Object.keys(taskType.properties)) { + let property = taskType.properties[key]; + schema.properties[key] = Objects.deepClone(property); + } } fixReferences(schema); taskDefinitions.push(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 7c92b359d10..e3599fccc70 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -13,11 +13,11 @@ import { QuickOpenHandler } from 'vs/workbench/parts/tasks/browser/taskQuickOpen import { TPromise } from 'vs/base/common/winjs.base'; import Severity from 'vs/base/common/severity'; import * as Objects from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { Action } from 'vs/base/common/actions'; import * as Dom from 'vs/base/browser/dom'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -31,7 +31,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -39,10 +39,11 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ProblemMatcherRegistry, NamedProblemMatcher } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IProgressService2, IProgressOptions, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService2, IProgressOptions, ProgressLocation } from 'vs/workbench/services/progress/common/progress'; + import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -53,10 +54,10 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { IStatusbarItem, IStatusbarRegistry, Extensions as StatusbarExtensions, StatusbarItemDescriptor, StatusbarAlignment } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarItem, IStatusbarRegistry, Extensions as StatusbarExtensions, StatusbarItemDescriptor } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; -import { IQuickOpenService, IPickOpenEntry, IPickOpenAction, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import Constants from 'vs/workbench/parts/markers/electron-browser/constants'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -74,7 +75,7 @@ import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, T import { Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, TaskEventKind, TaskSet, TaskGroup, GroupType, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskSorter, TaskIdentifier, KeyedTaskIdentifier + TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, ITaskProvider, RunOptions, CustomizationProperties, TaskFilter } from 'vs/workbench/parts/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -89,6 +90,7 @@ import { QuickOpenActionContributor } from '../browser/quickOpen'; import { Themable, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; let tasksCategory = nls.localize('tasksCategory', "Tasks"); @@ -118,7 +120,7 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { } private registerListeners(): void { - this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } protected updateStyles(): void { @@ -252,11 +254,9 @@ class BuildStatusBarItem extends Themable implements IStatusbarItem { this.updateStyles(); - return { - dispose: () => { - callOnDispose = dispose(callOnDispose); - } - }; + return toDisposable(() => { + callOnDispose = dispose(callOnDispose); + }); } private ignoreEvent(event: TaskEvent): boolean { @@ -425,7 +425,7 @@ class TaskMap { } } -interface TaskQuickPickEntry extends IPickOpenEntry { +interface TaskQuickPickEntry extends IQuickPickItem { task: Task; } @@ -458,6 +458,8 @@ class TaskService implements ITaskService { private _taskSystemListener: IDisposable; private _recentlyUsedTasks: LinkedMap; + private _taskRunningState: IContextKey; + private _outputChannel: IOutputChannel; private readonly _onDidStateChange: Emitter; @@ -473,7 +475,7 @@ class TaskService implements ITaskService { @ILifecycleService lifecycleService: ILifecycleService, @IModelService private modelService: IModelService, @IExtensionService private extensionService: IExtensionService, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, @ITerminalService private terminalService: ITerminalService, @IStorageService private storageService: IStorageService, @@ -481,7 +483,9 @@ class TaskService implements ITaskService { @IOpenerService private openerService: IOpenerService, @IWindowService private readonly _windowService: IWindowService, @IDialogService private dialogService: IDialogService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { this._configHasErrors = false; this._workspaceTasksPromise = undefined; @@ -520,6 +524,7 @@ class TaskService implements ITaskService { this.updateSetup(folderSetup); this.updateWorkspaceTasks(); }); + this._taskRunningState = TASK_RUNNING_STATE.bindTo(contextKeyService); lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown())); this._onDidStateChange = new Emitter(); this.registerCommands(); @@ -562,7 +567,7 @@ class TaskService implements ITaskService { KeybindingsRegistry.registerKeybindingRule({ id: 'workbench.action.tasks.build', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B }); @@ -867,12 +872,12 @@ class TaskService implements ITaskService { } private attachProblemMatcher(task: ContributedTask | CustomTask): TPromise { - interface ProblemMatcherPickEntry extends IPickOpenEntry { + interface ProblemMatcherPickEntry extends IQuickPickItem { matcher: NamedProblemMatcher; never?: boolean; learnMore?: boolean; } - let entries: ProblemMatcherPickEntry[] = []; + let entries: QuickPickInput[] = []; for (let key of ProblemMatcherRegistry.keys()) { let matcher = ProblemMatcherRegistry.get(key); if (matcher.deprecated) { @@ -890,15 +895,14 @@ class TaskService implements ITaskService { } if (entries.length > 0) { entries = entries.sort((a, b) => a.label.localeCompare(b.label)); - entries[0].separator = { border: true, label: nls.localize('TaskService.associate', 'associate') }; + entries.unshift({ type: 'separator', label: nls.localize('TaskService.associate', 'associate') }); entries.unshift( { label: nls.localize('TaskService.attachProblemMatcher.continueWithout', 'Continue without scanning the task output'), matcher: undefined }, { label: nls.localize('TaskService.attachProblemMatcher.never', 'Never scan the task output'), matcher: undefined, never: true }, { label: nls.localize('TaskService.attachProblemMatcher.learnMoreAbout', 'Learn more about scanning the task output'), matcher: undefined, learnMore: true } ); - return this.quickOpenService.pick(entries, { + return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectProblemMatcher', 'Select for which kind of errors and warnings to scan the task output'), - autoFocus: { autoFocusFirstEntry: true } }).then((selected) => { if (selected) { if (selected.learnMore) { @@ -1061,15 +1065,15 @@ class TaskService implements ITaskService { this.editorService.openEditor({ resource, options: { - forceOpen: true, - pinned: false + pinned: false, + forceReload: true // because content might have changed } }); } }); } - private writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: any): TPromise { + private writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: any): TPromise { if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { return this.configurationService.updateValue(key, value, { resource: workspaceFolder.uri }, ConfigurationTarget.WORKSPACE); } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { @@ -1084,7 +1088,6 @@ class TaskService implements ITaskService { return this.editorService.openEditor({ resource, options: { - forceOpen: true, pinned: false } }).then(() => undefined); @@ -1292,13 +1295,18 @@ class TaskService implements ITaskService { this._taskSystem = system; } this._taskSystemListener = this._taskSystem.onDidStateChange((event) => { + if (this._taskSystem) { + this._taskRunningState.set(this._taskSystem.isActiveSync()); + } this._onDidStateChange.fire(event); }); return this._taskSystem; } private getGroupedTasks(): TPromise { - return this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask').then(() => { + return TPromise.join([this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask'), TaskDefinitionRegistry.onReady()]).then(() => { + let validTypes: IStringDictionary = Object.create(null); + TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); return new TPromise((resolve, reject) => { let result: TaskSet[] = []; let counter: number = 0; @@ -1327,7 +1335,7 @@ class TaskService implements ITaskService { if (this.schemaVersion === JsonSchemaVersion.V2_0_0 && this._providers.size > 0) { this._providers.forEach((provider) => { counter++; - provider.provideTasks().done(done, error); + provider.provideTasks(validTypes).then(done, error); }); } else { resolve(result); @@ -1797,32 +1805,13 @@ class TaskService implements ITaskService { } return { label: task._label, description, task }; }; - let taskService = this; - let action = new class extends Action implements IPickOpenAction { - constructor() { - super('configureAction', 'Configure Task', 'quick-open-task-configure', true); + function fillEntries(entries: QuickPickInput[], tasks: Task[], groupLabel: string): void { + if (tasks.length) { + entries.push({ type: 'separator', label: groupLabel }); } - public run(item: IPickOpenItem): TPromise { - let task: Task = item.getPayload(); - taskService.quickOpenService.close(); - if (ContributedTask.is(task)) { - taskService.customize(task, undefined, true); - } else if (CustomTask.is(task)) { - taskService.openConfig(task); - } - return TPromise.as(false); - } - }; - function fillEntries(entries: TaskQuickPickEntry[], tasks: Task[], groupLabel: string, withBorder: boolean = false): void { - let first = true; for (let task of tasks) { let entry: TaskQuickPickEntry = TaskQuickPickEntry(task); - if (first) { - first = false; - entry.separator = { label: groupLabel, border: withBorder }; - } - entry.action = action; - entry.payload = task; + entry.buttons = [{ iconClass: 'quick-open-task-configure', tooltip: nls.localize('configureTask', "Configure Task") }]; entries.push(entry); } } @@ -1860,13 +1849,11 @@ class TaskService implements ITaskService { } } const sorter = this.createSorter(); - let hasRecentlyUsed: boolean = recent.length > 0; fillEntries(entries, recent, nls.localize('recentlyUsed', 'recently used tasks')); configured = configured.sort((a, b) => sorter.compare(a, b)); - let hasConfigured = configured.length > 0; - fillEntries(entries, configured, nls.localize('configured', 'configured tasks'), hasRecentlyUsed); + fillEntries(entries, configured, nls.localize('configured', 'configured tasks')); detected = detected.sort((a, b) => sorter.compare(a, b)); - fillEntries(entries, detected, nls.localize('detected', 'detected tasks'), hasRecentlyUsed || hasConfigured); + fillEntries(entries, detected, nls.localize('detected', 'detected tasks')); } } else { if (sort) { @@ -1886,12 +1873,24 @@ class TaskService implements ITaskService { return tasks.then((tasks) => this.createTaskQuickPickEntries(tasks, group, sort)); } }; - return this.quickOpenService.pick(_createEntries().then((entries) => { + return this.quickInputService.pick(_createEntries().then((entries) => { if (entries.length === 0 && defaultEntry) { entries.push(defaultEntry); } return entries; - }), { placeHolder, autoFocus: { autoFocusFirstEntry: true }, matchOnDescription: true }).then(entry => entry ? entry.task : undefined); + }), { + placeHolder, + matchOnDescription: true, + onDidTriggerItemButton: context => { + let task = context.item.task; + this.quickInputService.cancel(); + if (ContributedTask.is(task)) { + this.customize(task, undefined, true); + } else if (CustomTask.is(task)) { + this.openConfig(task); + } + } + }).then(entry => entry ? entry.task : undefined); } private showIgnoredFoldersMessage(): TPromise { @@ -2197,7 +2196,7 @@ class TaskService implements ITaskService { if (stat) { return stat.resource; } - return this.quickOpenService.pick(getTaskTemplates(), { placeHolder: nls.localize('TaskService.template', 'Select a Task Template') }).then((selection) => { + return this.quickInputService.pick(getTaskTemplates(), { placeHolder: nls.localize('TaskService.template', 'Select a Task Template') }).then((selection) => { if (!selection) { return undefined; } @@ -2228,7 +2227,6 @@ class TaskService implements ITaskService { this.editorService.openEditor({ resource, options: { - forceOpen: true, pinned: configFileCreated // pin only if config file is created #8727 } }); @@ -2245,8 +2243,8 @@ class TaskService implements ITaskService { } }; - function isTaskEntry(value: IPickOpenEntry): value is IPickOpenEntry & { task: Task } { - let candidate: IPickOpenEntry & { task: Task } = value as any; + function isTaskEntry(value: IQuickPickItem): value is IQuickPickItem & { task: Task } { + let candidate: IQuickPickItem & { task: Task } = value as any; return candidate && !!candidate.task; } @@ -2258,8 +2256,8 @@ class TaskService implements ITaskService { let openLabel = nls.localize('TaskService.openJsonFile', 'Open tasks.json file'); let entries = TPromise.join(stats).then((stats) => { return taskPromise.then((taskMap) => { - type EntryType = (IPickOpenEntry & { task: Task; }) | (IPickOpenEntry & { folder: IWorkspaceFolder; }); - let entries: EntryType[] = []; + type EntryType = (IQuickPickItem & { task: Task; }) | (IQuickPickItem & { folder: IWorkspaceFolder; }); + let entries: QuickPickInput[] = []; if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { let tasks = taskMap.all(); let needsCreateOrOpen: boolean = true; @@ -2274,7 +2272,10 @@ class TaskService implements ITaskService { } if (needsCreateOrOpen) { let label = stats[0] !== void 0 ? openLabel : createLabel; - entries.push({ label, folder: this.contextService.getWorkspace().folders[0], separator: entries.length > 0 ? { border: true } : undefined }); + if (entries.length) { + entries.push({ type: 'separator' }); + } + entries.push({ label, folder: this.contextService.getWorkspace().folders[0] }); } } else { let folders = this.contextService.getWorkspace().folders; @@ -2286,14 +2287,14 @@ class TaskService implements ITaskService { for (let i = 0; i < tasks.length; i++) { let entry: EntryType = { label: tasks[i]._label, task: tasks[i], description: folder.name }; if (i === 0) { - entry.separator = { label: folder.name, border: index > 0 }; + entries.push({ type: 'separator', label: folder.name }); } entries.push(entry); } } else { let label = stats[index] !== void 0 ? openLabel : createLabel; let entry: EntryType = { label, folder: folder }; - entry.separator = { label: folder.name, border: index > 0 }; + entries.push({ type: 'separator', label: folder.name }); entries.push(entry); } index++; @@ -2303,8 +2304,8 @@ class TaskService implements ITaskService { }); }); - this.quickOpenService.pick(entries, - { placeHolder: nls.localize('TaskService.pickTask', 'Select a task to configure'), autoFocus: { autoFocusFirstEntry: true } }). + this.quickInputService.pick(entries, + { placeHolder: nls.localize('TaskService.pickTask', 'Select a task to configure') }). then((selection) => { if (!selection) { return; @@ -2419,6 +2420,75 @@ class TaskService implements ITaskService { } } +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '2_run', + command: { + id: 'workbench.action.tasks.runTask', + title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '2_run', + command: { + id: 'workbench.action.tasks.build', + title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") + }, + order: 2 +}); + +// Manage Tasks +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '3_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.showTasks', + title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '3_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.restartTask', + title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") + }, + order: 2 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '3_manage', + command: { + precondition: TASK_RUNNING_STATE, + id: 'workbench.action.tasks.terminate', + title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") + }, + order: 3 +}); + +// Configure Tasks +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '4_configure', + command: { + id: 'workbench.action.tasks.configureTaskRunner', + title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: '4_configure', + command: { + id: 'workbench.action.tasks.configureDefaultBuildTask', + title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") + }, + order: 2 +}); + + MenuRegistry.addCommand({ id: ConfigureTaskAction.ID, title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.showLog', title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, category: { value: tasksCategory, original: 'Tasks' } }); MenuRegistry.addCommand({ id: 'workbench.action.tasks.runTask', title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, category: { value: tasksCategory, original: 'Tasks' } }); @@ -2434,7 +2504,7 @@ MenuRegistry.addCommand({ id: 'workbench.action.tasks.configureDefaultTestTask', // Tasks Output channel. Register it before using it in Task Service. let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); -outputChannelRegistry.registerChannel(TaskService.OutputChannelId, TaskService.OutputChannelLabel); +outputChannelRegistry.registerChannel({ id: TaskService.OutputChannelId, label: TaskService.OutputChannelLabel, log: false }); // Task Service registerSingleton(ITaskService, TaskService); @@ -2486,6 +2556,8 @@ let schema: IJSONSchema = { import schemaVersion1 from './jsonSchema_v1'; import schemaVersion2 from './jsonSchema_v2'; +import { TaskDefinitionRegistry } from 'vs/workbench/parts/tasks/common/taskDefinitionRegistry'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; schema.definitions = { ...schemaVersion1.definitions, ...schemaVersion2.definitions, diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index b60cd559cb7..cc5ca579552 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -316,7 +316,7 @@ export class TerminalTaskSystem implements ITaskSystem { resolvedVariables = TPromise.as(result); } return resolvedVariables.then((variables) => { - return this.executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, undefined, variables, this.configurationResolverService)); + return this.executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, taskSystemInfo, variables, this.configurationResolverService)); }); } @@ -329,9 +329,9 @@ export class TerminalTaskSystem implements ITaskSystem { promise = new TPromise((resolve, reject) => { const problemMatchers = this.resolveMatchers(resolver, task.problemMatchers); let watchingProblemMatcher = new WatchingProblemCollector(problemMatchers, this.markerService, this.modelService); - let toUnbind: IDisposable[] = []; + let toDispose: IDisposable[] = []; let eventCounter: number = 0; - toUnbind.push(watchingProblemMatcher.onDidStateChange((event) => { + toDispose.push(watchingProblemMatcher.onDidStateChange((event) => { if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) { eventCounter++; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); @@ -354,7 +354,7 @@ export class TerminalTaskSystem implements ITaskSystem { return; } let processStartedSignaled: boolean = false; - terminal.processReady.done(() => { + terminal.processReady.then(() => { processStartedSignaled = true; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId)); }, (_error) => { @@ -397,8 +397,8 @@ export class TerminalTaskSystem implements ITaskSystem { if (processStartedSignaled) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); } - toUnbind = dispose(toUnbind); - toUnbind = null; + toDispose = dispose(toDispose); + toDispose = null; for (let i = 0; i < eventCounter; i++) { let event = TaskEvent.create(TaskEventKind.Inactive, task); this._onDidStateChange.fire(event); @@ -415,7 +415,7 @@ export class TerminalTaskSystem implements ITaskSystem { return; } let processStartedSignaled: boolean = false; - terminal.processReady.done(() => { + terminal.processReady.then(() => { processStartedSignaled = true; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId)); }, (_error) => { @@ -517,6 +517,7 @@ export class TerminalTaskSystem implements ITaskSystem { } private createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver): [ITerminalInstance, string, TaskError | undefined] { + let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; let options = this.resolveOptions(resolver, task.command.options); let { command, args } = this.resolveCommandAndArgs(resolver, task.command); let commandExecutable = CommandString.value(command); @@ -537,24 +538,25 @@ export class TerminalTaskSystem implements ITaskSystem { let isShellCommand = task.command.runtime === RuntimeType.Shell; if (isShellCommand) { shellLaunchConfig = { name: terminalName, executable: null, args: null, waitOnExit }; + this.terminalService.configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig, platform); let shellSpecified: boolean = false; let shellOptions: ShellConfiguration = task.command.options && task.command.options.shell; - if (shellOptions && shellOptions.executable) { - shellLaunchConfig.executable = this.resolveVariable(resolver, shellOptions.executable); - shellSpecified = true; + if (shellOptions) { + if (shellOptions.executable) { + shellLaunchConfig.executable = this.resolveVariable(resolver, shellOptions.executable); + shellSpecified = true; + } if (shellOptions.args) { shellLaunchConfig.args = this.resolveVariables(resolver, shellOptions.args.slice()); } else { shellLaunchConfig.args = []; } - } else { - this.terminalService.configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); } let shellArgs = shellLaunchConfig.args.slice(0); let toAdd: string[] = []; let commandLine = this.buildShellCommandLine(shellLaunchConfig.executable, shellOptions, command, args); let windowsShellArgs: boolean = false; - if (Platform.isWindows) { + if (platform === Platform.Platform.Windows) { // Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's // safe to assume that this was used by accident as Sysnative does not // exist and will break the terminal in non-WoW64 environments. @@ -641,14 +643,23 @@ export class TerminalTaskSystem implements ITaskSystem { } if (options.cwd) { let cwd = options.cwd; - if (!path.isAbsolute(cwd)) { + let p: typeof path; + // This must be normalized to the OS + if (platform === Platform.Platform.Windows) { + p = path.win32 as any; + } else if (platform === Platform.Platform.Linux || platform === Platform.Platform.Mac) { + p = path.posix as any; + } else { + p = path; + } + if (!p.isAbsolute(cwd)) { let workspaceFolder = Task.getWorkspaceFolder(task); if (workspaceFolder.uri.scheme === 'file') { - cwd = path.join(workspaceFolder.uri.fsPath, cwd); + cwd = p.join(workspaceFolder.uri.fsPath, cwd); } } // This must be normalized to the OS - shellLaunchConfig.cwd = path.normalize(cwd); + shellLaunchConfig.cwd = p.normalize(cwd); } if (options.env) { shellLaunchConfig.env = options.env; @@ -959,13 +970,13 @@ export class TerminalTaskSystem implements ITaskSystem { } let taskSystemInfo: TaskSystemInfo = resolver.taskSystemInfo; let hasFilePrefix = matcher.filePrefix !== void 0; - let hasScheme = taskSystemInfo !== void 0 && taskSystemInfo.fileSystemScheme !== void 0 && taskSystemInfo.fileSystemScheme !== 'file'; - if (!hasFilePrefix && !hasScheme) { + let hasUriProvider = taskSystemInfo !== void 0 && taskSystemInfo.uriProvider !== void 0; + if (!hasFilePrefix && !hasUriProvider) { result.push(matcher); } else { let copy = Objects.deepClone(matcher); - if (hasScheme) { - copy.fileSystemScheme = taskSystemInfo.fileSystemScheme; + if (hasUriProvider) { + copy.uriProvider = taskSystemInfo.uriProvider; } if (hasFilePrefix) { copy.filePrefix = this.resolveVariable(resolver, copy.filePrefix); diff --git a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts index 8169d1f3c8c..601a2db9a36 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerDetector.ts @@ -12,7 +12,7 @@ import * as Strings from 'vs/base/common/strings'; import * as Collections from 'vs/base/common/collections'; import { CommandOptions, Source, ErrorData } from 'vs/base/common/processes'; -import { LineProcess } from 'vs/base/node/processes'; +import { LineProcess, LineData } from 'vs/base/node/processes'; import { IFileService } from 'vs/platform/files/common/files'; @@ -39,7 +39,7 @@ interface TaskInfos { interface TaskDetectorMatcher { init(): void; - match(tasks: string[], line: string); + match(tasks: string[], line: string): void; } interface DetectorConfig { @@ -57,7 +57,7 @@ class RegexpTaskMatcher implements TaskDetectorMatcher { init() { } - match(tasks: string[], line: string) { + match(tasks: string[], line: string): void { let matches = this.regexp.exec(line); if (matches && matches.length > 0) { tasks.push(matches[1]); @@ -76,7 +76,7 @@ class GruntTaskMatcher implements TaskDetectorMatcher { this.descriptionOffset = null; } - match(tasks: string[], line: string) { + match(tasks: string[], line: string): void { // grunt lists tasks as follows (description is wrapped into a new line if too long): // ... // Available tasks @@ -269,7 +269,17 @@ export class ProcessRunnerDetector { private runDetection(process: LineProcess, command: string, isShellCommand: boolean, matcher: TaskDetectorMatcher, problemMatchers: string[], list: boolean): TPromise { let tasks: string[] = []; matcher.init(); - return process.start().then((success) => { + + const onProgress = (progress: LineData) => { + if (progress.source === Source.stderr) { + this._stderr.push(progress.line); + return; + } + let line = Strings.removeAnsiEscapeCodes(progress.line); + matcher.match(tasks, line); + }; + + return process.start(onProgress).then((success) => { if (tasks.length === 0) { if (success.cmdCode !== 0) { if (command === 'gulp') { @@ -305,16 +315,6 @@ export class ProcessRunnerDetector { this._stderr.push(nls.localize('TaskSystemDetector.noProgram', 'Program {0} was not found. Message is {1}', command, error.message)); } return { config: null, stdout: this._stdout, stderr: this._stderr }; - }, (progress) => { - if (progress.source === Source.stderr) { - this._stderr.push(progress.line); - return; - } - let line = Strings.removeAnsiEscapeCodes(progress.line); - let matches = matcher.match(tasks, line); - if (matches && matches.length > 0) { - tasks.push(matches[1]); - } }); } diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 27a7ca1e08b..8380beb70b3 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -233,9 +233,9 @@ export class ProcessTaskSystem implements ITaskSystem { } if (task.isBackground) { let watchingProblemMatcher = new WatchingProblemCollector(this.resolveMatchers(task, task.problemMatchers), this.markerService, this.modelService); - let toUnbind: IDisposable[] = []; + let toDispose: IDisposable[] = []; let eventCounter: number = 0; - toUnbind.push(watchingProblemMatcher.onDidStateChange((event) => { + toDispose.push(watchingProblemMatcher.onDidStateChange((event) => { if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) { eventCounter++; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); @@ -249,7 +249,21 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress: LineData) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + watchingProblemMatcher.processLine(line); + if (delayer === null) { + delayer = new Async.Delayer(3000); + } + delayer.trigger(() => { + watchingProblemMatcher.forceDelivery(); + return null; + }).then(() => { + delayer = null; + }); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -263,8 +277,8 @@ export class ProcessTaskSystem implements ITaskSystem { if (processStartedSignaled) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, success.cmdCode)); } - toUnbind = dispose(toUnbind); - toUnbind = null; + toDispose = dispose(toDispose); + toDispose = null; for (let i = 0; i < eventCounter; i++) { this._onDidStateChange.fire(inactiveEvent); } @@ -280,26 +294,13 @@ export class ProcessTaskSystem implements ITaskSystem { }, (error: ErrorData) => { this.childProcessEnded(); watchingProblemMatcher.dispose(); - toUnbind = dispose(toUnbind); - toUnbind = null; + toDispose = dispose(toDispose); + toDispose = null; for (let i = 0; i < eventCounter; i++) { this._onDidStateChange.fire(inactiveEvent); } eventCounter = 0; return this.handleError(task, error); - }, (progress: LineData) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - watchingProblemMatcher.processLine(line); - if (delayer === null) { - delayer = new Async.Delayer(3000); - } - delayer.trigger(() => { - watchingProblemMatcher.forceDelivery(); - return null; - }).then(() => { - delayer = null; - }); }); let result: ITaskExecuteResult = (task).tscWatch ? { kind: TaskExecuteKind.Started, started: { restartOnFileChanges: '**/*.ts' }, promise: this.activeTaskPromise } @@ -312,7 +313,12 @@ export class ProcessTaskSystem implements ITaskSystem { this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); let processStartedSignaled: boolean = false; - const startPromise = this.childProcess.start(); + const onProgress = (progress) => { + let line = Strings.removeAnsiEscapeCodes(progress.line); + this.outputChannel.append(line + '\n'); + startStopProblemMatcher.processLine(line); + }; + const startPromise = this.childProcess.start(onProgress); this.childProcess.pid.then(pid => { if (pid !== -1) { processStartedSignaled = true; @@ -340,10 +346,6 @@ export class ProcessTaskSystem implements ITaskSystem { this._onDidStateChange.fire(inactiveEvent); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); return this.handleError(task, error); - }, (progress) => { - let line = Strings.removeAnsiEscapeCodes(progress.line); - this.outputChannel.append(line + '\n'); - startStopProblemMatcher.processLine(line); }); return { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; } @@ -359,7 +361,7 @@ export class ProcessTaskSystem implements ITaskSystem { let makeVisible = false; if (errorData.error && !errorData.terminated) { let args: string = task.command.args ? task.command.args.join(' ') : ''; - this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', task.command.name, args)); + this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', JSON.stringify(task.command.name), args)); this.outputChannel.append(errorData.error.message); makeVisible = true; } diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index 21a503e8059..4cbe7cc1bf3 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -25,7 +25,7 @@ import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; import { TaskDefinition } from 'vs/workbench/parts/tasks/node/tasks'; -export enum ShellQuoting { +export const enum ShellQuoting { /** * Default is character escaping. */ @@ -63,7 +63,7 @@ export interface ShellQuotingOptions { } export interface ShellConfiguration { - executable: string; + executable?: string; args?: string[]; quoting?: ShellQuotingOptions; } @@ -637,14 +637,17 @@ namespace ShellConfiguration { export function is(value: any): value is ShellConfiguration { let candidate: ShellConfiguration = value; - return candidate && Types.isString(candidate.executable) && (candidate.args === void 0 || Types.isStringArray(candidate.args)); + return candidate && (Types.isString(candidate.executable) || Types.isStringArray(candidate.args)); } export function from(this: void, config: ShellConfiguration, context: ParseContext): Tasks.ShellConfiguration { if (!is(config)) { return undefined; } - let result: ShellConfiguration = { executable: config.executable }; + let result: ShellConfiguration = {}; + if (config.executable !== void 0) { + result.executable = config.executable; + } if (config.args !== void 0) { result.args = config.args.slice(); } @@ -887,7 +890,12 @@ namespace CommandConfiguration { if (converted !== void 0) { result.args.push(converted); } else { - context.problemReporter.error(nls.localize('ConfigurationParser.inValidArg', 'Error: command argument must either be a string or a quoted string. Provided value is:\n{0}', context.problemReporter.error(nls.localize('ConfigurationParser.noargs', 'Error: command arguments must be an array of strings. Provided value is:\n{0}', arg ? JSON.stringify(arg, undefined, 4) : 'undefined')))); + context.problemReporter.error( + nls.localize( + 'ConfigurationParser.inValidArg', + 'Error: command argument must either be a string or a quoted string. Provided value is:\n{0}', + arg ? JSON.stringify(arg, undefined, 4) : 'undefined' + )); } } } @@ -1177,7 +1185,7 @@ namespace ConfigurationProperties { if (Types.isArray(external.dependsOn)) { result.dependsOn = external.dependsOn.map(item => TaskDependency.from(item, context)); } else { - result.dependsOn = [TaskDependency.from(external, context)]; + result.dependsOn = [TaskDependency.from(external.dependsOn, context)]; } } if (includeCommandOptions && (external.presentation !== void 0 || (external as LegacyCommandProperties).terminal !== void 0)) { @@ -1254,7 +1262,7 @@ namespace ConfiguringTask { if (taskIdentifier === void 0) { context.problemReporter.error(nls.localize( 'ConfigurationParser.incorrectType', - 'Error: the task configuration \'{0}\' is using and unknown type. The task configuration will be ignored.', JSON.stringify(external, undefined, 0) + 'Error: the task configuration \'{0}\' is using an unknown type. The task configuration will be ignored.', JSON.stringify(external, undefined, 0) )); return undefined; } diff --git a/src/vs/workbench/parts/tasks/node/tasks.ts b/src/vs/workbench/parts/tasks/node/tasks.ts index c0cd15ad06d..a24e94c013a 100644 --- a/src/vs/workbench/parts/tasks/node/tasks.ts +++ b/src/vs/workbench/parts/tasks/node/tasks.ts @@ -27,7 +27,10 @@ namespace TaskDefinition { export function createTaskIdentifier(external: TaskIdentifier, reporter: { error(message: string): void; }): KeyedTaskIdentifier | undefined { let definition = TaskDefinitionRegistry.get(external.type); if (definition === void 0) { - return undefined; + // We have no task definition so we can't sanitize the literal. Take it as is + let copy = Objects.deepClone(external); + delete copy._key; + return KeyedTaskIdentifier.create(copy); } let literal: { type: string;[name: string]: any } = Object.create(null); diff --git a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts index 5a20b10de7d..8100e22fb60 100644 --- a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as assert from 'assert'; import Severity from 'vs/base/common/severity'; import * as UUID from 'vs/base/common/uuid'; diff --git a/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts index 7558aa6baef..4369e7c078b 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts @@ -7,9 +7,6 @@ import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED } from 'vs/workbench/parts/terminal/common/terminal'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey; @@ -17,23 +14,19 @@ export class TerminalFindWidget extends SimpleFindWidget { constructor( @IContextViewService _contextViewService: IContextViewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ITerminalService private readonly _terminalService: ITerminalService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService notificationService: INotificationService, - @IStorageService storageService: IStorageService + @ITerminalService private readonly _terminalService: ITerminalService ) { - super(_contextViewService, _contextKeyService, keybindingService, notificationService, storageService); + super(_contextViewService, _contextKeyService); this._findInputFocused = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService); } public find(previous: boolean) { - let val = this.inputValue; - let instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.getActiveInstance(); if (instance !== null) { if (previous) { - instance.findPrevious(val); + instance.findPrevious(this.inputValue); } else { - instance.findNext(val); + instance.findNext(this.inputValue); } } } diff --git a/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts b/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts index f08c16eb73f..fa2f6e78e84 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalQuickOpen.ts @@ -13,6 +13,7 @@ import { ContributableActionProvider } from 'vs/workbench/browser/actions'; import { stripWildcards } from 'vs/base/common/strings'; import { matchesFuzzy } from 'vs/base/common/filters'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class TerminalEntry extends QuickOpenEntry { @@ -83,7 +84,7 @@ export class TerminalPickerHandler extends QuickOpenHandler { super(); } - public getResults(searchValue: string): TPromise { + public getResults(searchValue: string, token: CancellationToken): TPromise { searchValue = searchValue.trim(); const normalizedSearchValueLowercase = stripWildcards(searchValue).toLowerCase(); diff --git a/src/vs/workbench/parts/terminal/browser/terminalTab.ts b/src/vs/workbench/parts/terminal/browser/terminalTab.ts index c14c3d449d7..651f27fb815 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalTab.ts @@ -7,7 +7,7 @@ import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITermin import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter, anyEvent } from 'vs/base/common/event'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { SplitView, Orientation, IView } from 'vs/base/browser/ui/splitview/splitview'; +import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { IPartService, Position } from 'vs/workbench/services/part/common/partService'; const SPLIT_PANE_MIN_SIZE = 120; @@ -19,10 +19,6 @@ class SplitPaneContainer { private _splitViewDisposables: IDisposable[]; private _children: SplitPane[] = []; - // If the user sizes the panes manually, the proportional resizing will not be applied. - // Proportional resizing will come back when: a sash is reset, an instance is added/removed or - // the panel position moves. - private _isManuallySized: boolean = false; private _onDidChange: Event = Event.None; public get onDidChange(): Event { return this._onDidChange; } @@ -40,28 +36,11 @@ class SplitPaneContainer { private _createSplitView(): void { this._splitView = new SplitView(this._container, { orientation: this.orientation }); this._splitViewDisposables = []; - this._splitViewDisposables.push(this._splitView.onDidSashReset(() => this.resetSize())); - this._splitViewDisposables.push(this._splitView.onDidSashChange(() => { - this._isManuallySized = true; - })); + this._splitViewDisposables.push(this._splitView.onDidSashReset(() => this._splitView.distributeViewSizes())); } public split(instance: ITerminalInstance, index: number = this._children.length): void { - const size = this.orientation === Orientation.HORIZONTAL ? this._width : this._height; - this._addChild(size / (this._children.length + 1), instance, index); - } - - public resetSize(): void { - // TODO: Optimize temrinal instance layout - let totalSize = 0; - for (let i = 0; i < this._splitView.length; i++) { - totalSize += this._splitView.getViewSize(i); - } - const newSize = Math.floor(totalSize / this._splitView.length); - for (let i = 0; i < this._splitView.length - 1; i++) { - this._splitView.resizeView(i, newSize); - } - this._isManuallySized = false; + this._addChild(instance, index); } public resizePane(index: number, direction: Direction, amount: number): void { @@ -111,10 +90,9 @@ class SplitPaneContainer { for (let i = 0; i < this._splitView.length - 1; i++) { this._splitView.resizeView(i, sizes[i]); } - this._isManuallySized = true; } - private _addChild(size: number, instance: ITerminalInstance, index: number): void { + private _addChild(instance: ITerminalInstance, index: number): void { const child = new SplitPane(instance, this.orientation === Orientation.HORIZONTAL ? this._height : this._width); child.orientation = this.orientation; if (typeof index === 'number') { @@ -123,9 +101,7 @@ class SplitPaneContainer { this._children.push(child); } - this._withDisabledLayout(() => this._splitView.addView(child, size, index)); - - this.resetSize(); + this._withDisabledLayout(() => this._splitView.addView(child, Sizing.Distribute, index)); this._onDidChange = anyEvent(...this._children.map(c => c.onDidChange)); } @@ -139,8 +115,7 @@ class SplitPaneContainer { } if (index !== null) { this._children.splice(index, 1); - this._splitView.removeView(index); - this.resetSize(); + this._splitView.removeView(index, Sizing.Distribute); } } @@ -154,9 +129,6 @@ class SplitPaneContainer { this._children.forEach(c => c.orthogonalLayout(width)); this._splitView.layout(height); } - if (!this._isManuallySized) { - this.resetSize(); - } } public setOrientation(orientation: Orientation): void { @@ -306,7 +278,7 @@ export class TerminalTab extends Disposable implements ITerminalTab { // Adjust focus if the instance was active if (wasActiveInstance && this._terminalInstances.length > 0) { - let newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1; + const newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1; this.setActiveInstanceByIndex(newIndex); // TODO: Only focus the new instance if the tab had focus? this.activeInstance.focus(true); @@ -422,10 +394,6 @@ export class TerminalTab extends Disposable implements ITerminalTab { } this._splitPaneContainer.layout(width, height); - - if (panelPositionChanged) { - this._splitPaneContainer.resetSize(); - } } } diff --git a/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts b/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts index e8d18df087e..26db47bc76e 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts @@ -7,7 +7,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; const WIDGET_HEIGHT = 29; -export class TerminalWidgetManager { +export class TerminalWidgetManager implements IDisposable { private _container: HTMLElement; private _xtermViewport: HTMLElement; @@ -24,6 +24,14 @@ export class TerminalWidgetManager { this._initTerminalHeightWatcher(terminalWrapper); } + public dispose(): void { + if (this._container) { + this._container.parentElement.removeChild(this._container); + this._container = null; + } + this._xtermViewport = null; + } + private _initTerminalHeightWatcher(terminalWrapper: HTMLElement) { // Watch the xterm.js viewport for style changes and do a layout if it changes this._xtermViewport = terminalWrapper.querySelector('.xterm-viewport'); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 4ca2847cbf8..07945f54cd2 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; import { RawContextKey, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -13,9 +14,11 @@ export const TERMINAL_PANEL_ID = 'workbench.panel.terminal'; export const TERMINAL_SERVICE_ID = 'terminalService'; -/** A context key that is set when the integrated terminal has focus. */ +/** A context key that is set when there is at least one opened integrated terminal. */ +export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey('terminalIsOpen', false); +/** A context key that is set when the integrated terminal has focus. */ export const KEYBINDING_CONTEXT_TERMINAL_FOCUS = new RawContextKey('terminalFocus', undefined); -/** A context key that is set when the integrated terminal does not have focus. */ +/** A context key that is set when the integrated terminal does not have focus. */ export const KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FOCUS.toNegated(); /** A keybinding context key that is set when the integrated terminal has text selected. */ @@ -36,6 +39,11 @@ export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWor export const NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY = 'terminal.integrated.neverSuggestSelectWindowsShell'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; +// The creation of extension host terminals is delayed by this value (milliseconds). The purpose of +// this delay is to allow the terminal instance to initialize correctly and have its ID set before +// trying to create the corressponding object on the ext host. +export const EXT_HOST_CREATION_DELAY = 100; + export const ITerminalService = createDecorator(TERMINAL_SERVICE_ID); export const TerminalCursorStyle = { @@ -64,10 +72,12 @@ export interface ITerminalConfiguration { windows: string[]; }; macOptionIsMeta: boolean; + macOptionClickForcesSelection: boolean; rendererType: 'auto' | 'canvas' | 'dom'; rightClickBehavior: 'default' | 'copyPaste' | 'selectWord'; cursorBlinking: boolean; cursorStyle: string; + drawBoldTextInBrightColors: boolean; fontFamily: string; fontWeight: FontWeight; fontWeightBold: FontWeight; @@ -87,7 +97,6 @@ export interface ITerminalConfiguration { windows: { [key: string]: string }; }; showExitAlert: boolean; - experimentalRestore: boolean; } export interface ITerminalConfigHelper { @@ -96,7 +105,7 @@ export interface ITerminalConfigHelper { /** * Merges the default shell path and args into the provided launch configuration */ - mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig): void; + mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride?: platform.Platform): void; /** Sets whether a workspace shell configuration is allowed or not */ setWorkspaceShellAllowed(isAllowed: boolean): void; } @@ -156,6 +165,12 @@ export interface IShellLaunchConfig { * of the terminal. Use \x1b over \033 or \e for the escape control character. */ initialText?: string; + + /** + * When true the terminal will be created with no process. This is primarily used to give + * extensions full control over the terminal. + */ + isRendererOnly?: boolean; } export interface ITerminalService { @@ -168,9 +183,11 @@ export interface ITerminalService { onInstanceCreated: Event; onInstanceDisposed: Event; onInstanceProcessIdReady: Event; + onInstanceDimensionsChanged: Event; onInstanceRequestExtHostProcess: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; + onActiveInstanceChanged: Event; terminalInstances: ITerminalInstance[]; terminalTabs: ITerminalTab[]; @@ -181,6 +198,12 @@ export interface ITerminalService { * default shell selection dialog may display. */ createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + + /** + * Creates a terminal renderer. + * @param name The name of the terminal. + */ + createTerminalRenderer(name: string): ITerminalInstance; /** * Creates a raw terminal instance, this should not be used outside of the terminal part. */ @@ -203,8 +226,6 @@ export interface ITerminalService { hidePanel(): void; focusFindWidget(): TPromise; hideFindWidget(): void; - showNextFindTermFindWidget(): void; - showPreviousFindTermFindWidget(): void; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; selectDefaultWindowsShell(): TPromise; @@ -238,12 +259,27 @@ export interface ITerminalTab { split(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; } +export interface ITerminalDimensions { + /** + * The columns of the terminal. + */ + readonly cols: number; + + /** + * The rows of the terminal. + */ + readonly rows: number; +} + export interface ITerminalInstance { /** * The ID of the terminal instance, this is an arbitrary number only used to identify the * terminal instance. */ - id: number; + readonly id: number; + + readonly cols: number; + readonly rows: number; /** * The process ID of the shell process, this is undefined when there is no process associated @@ -267,6 +303,42 @@ export interface ITerminalInstance { onRequestExtHostProcess: Event; + onDimensionsChanged: Event; + + onFocus: Event; + + /** + * Attach a listener to the raw data stream coming from the pty, including ANSI escape + * sequences. + */ + onData: Event; + + /** + * Attach a listener to the "renderer" input event, this event fires for terminal renderers on + * keystrokes and when the Terminal.sendText extension API is used. + * @param listener The listener function. + */ + onRendererInput: Event; + + /** + * Attach a listener to listen for new lines added to this terminal instance. + * + * @param listener The listener function which takes new line strings added to the terminal, + * excluding ANSI escape sequences. The line event will fire when an LF character is added to + * the terminal (ie. the line is not wrapped). Note that this means that the line data will + * not fire for the last line, until either the line is ended with a LF character of the process + * is exited. The lineData string will contain the fully wrapped line, not containing any LF/CR + * characters. + */ + onLineData: Event; + + /** + * Attach a listener that fires when the terminal's pty process exits. The number in the event + * is the processes' exit code, an exit code of null means the process was killed as a result of + * the ITerminalInstance being disposed. + */ + onExit: Event; + processReady: TPromise; /** @@ -293,7 +365,7 @@ export interface ITerminalInstance { /** * The shell launch config used to launch the shell. */ - shellLaunchConfig: IShellLaunchConfig; + readonly shellLaunchConfig: IShellLaunchConfig; /** * Whether to disable layout for the terminal. This is useful when the size of the terminal is @@ -310,8 +382,11 @@ export interface ITerminalInstance { /** * Dispose the terminal instance, removing it from the panel/service and freeing up resources. + * + * @param isShuttingDown Whether VS Code is shutting down, if so kill any terminal processes + * immediately. */ - dispose(): void; + dispose(isShuttingDown?: boolean): void; /** * Registers a link matcher, allowing custom link patterns to be matched and handled. @@ -374,12 +449,20 @@ export interface ITerminalInstance { notifyFindWidgetFocusChanged(isFocused: boolean): void; /** - * Focuses the terminal instance. + * Focuses the terminal instance if it's able to (xterm.js instance exists). * * @param focus Force focus even if there is a selection. */ focus(force?: boolean): void; + /** + * Focuses the terminal instance when it's ready (the xterm.js instance is created). Use this + * when the terminal is being shown. + * + * @param focus Force focus even if there is a selection. + */ + focusWhenReady(force?: boolean): Promise; + /** * Focuses and pastes the contents of the clipboard into the terminal instance. */ @@ -396,6 +479,12 @@ export interface ITerminalInstance { */ sendText(text: string, addNewLine: boolean): void; + /** + * Write text directly to the terminal, skipping the process if it exists. + * @param text The text to write. + */ + write(text: string): void; + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ @@ -447,33 +536,6 @@ export interface ITerminalInstance { */ setVisible(visible: boolean): void; - /** - * Attach a listener to the raw data stream coming from the pty, including ANSI escape - * sequecnes. - * @param listener The listener function. - */ - onData(listener: (data: string) => void): IDisposable; - - /** - * Attach a listener to listen for new lines added to this terminal instance. - * - * @param listener The listener function which takes new line strings added to the terminal, - * excluding ANSI escape sequences. The line event will fire when an LF character is added to - * the terminal (ie. the line is not wrapped). Note that this means that the line data will - * not fire for the last line, until either the line is ended with a LF character of the process - * is exited. The lineData string will contain the fully wrapped line, not containing any LF/CR - * characters. - */ - onLineData(listener: (lineData: string) => void): IDisposable; - - /** - * Attach a listener that fires when the terminal's pty process exits. - * - * @param listener The listener function which takes the processes' exit code, an exit code of - * null means the process was killed as a result of the ITerminalInstance being disposed. - */ - onExit(listener: (exitCode: number) => void): IDisposable; - /** * Immediately kills the terminal's current pty process and launches a new one to replace it. * @@ -486,7 +548,11 @@ export interface ITerminalInstance { */ setTitle(title: string, eventFromProcess: boolean): void; + setDimensions(dimensions: ITerminalDimensions): void; + addDisposable(disposable: IDisposable): void; + + toggleEscapeSequenceLogging(): void; } export interface ITerminalCommandTracker { @@ -510,12 +576,13 @@ export interface ITerminalProcessManager extends IDisposable { readonly onProcessExit: Event; addDisposable(disposable: IDisposable); + dispose(immediate?: boolean); createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number); write(data: string): void; setDimensions(cols: number, rows: number): void; } -export enum ProcessState { +export const enum ProcessState { // The process has not been initialized yet. UNINITIALIZED, // The process is currently launching, the process is marked as launching @@ -543,9 +610,9 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { emitPid(pid: number): void; emitExit(exitCode: number): void; - onInput(listener: (data: string) => void): void; - onResize(listener: (cols: number, rows: number) => void): void; - onShutdown(listener: () => void): void; + onInput: Event; + onResize: Event<{ cols: number, rows: number }>; + onShutdown: Event; } export interface ITerminalProcessExtHostRequest { diff --git a/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts index 0d5fb389afa..f002de9397b 100644 --- a/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts @@ -160,9 +160,9 @@ const ansiColorMap = { }; export function registerColors(): void { - for (let id in ansiColorMap) { - let entry = ansiColorMap[id]; - let colorName = id.substring(13); + for (const id in ansiColorMap) { + const entry = ansiColorMap[id]; + const colorName = id.substring(13); ansiColorIdentifiers[entry.index] = registerColor(id, entry.defaults, nls.localize('terminal.ansiColor', '\'{0}\' ANSI color in the terminal.', colorName)); } } diff --git a/src/vs/workbench/parts/terminal/common/terminalCommands.ts b/src/vs/workbench/parts/terminal/common/terminalCommands.ts index 828588d03b1..89781821f83 100644 --- a/src/vs/workbench/parts/terminal/common/terminalCommands.ts +++ b/src/vs/workbench/parts/terminal/common/terminalCommands.ts @@ -3,10 +3,63 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; -export function setup(): void { +export const enum TERMINAL_COMMAND_ID { + TOGGLE = 'workbench.action.terminal.toggleTerminal', + KILL = 'workbench.action.terminal.kill', + QUICK_KILL = 'workbench.action.terminal.quickKill', + COPY_SELECTION = 'workbench.action.terminal.copySelection', + SELECT_ALL = 'workbench.action.terminal.selectAll', + DELETE_WORD_LEFT = 'workbench.action.terminal.deleteWordLeft', + DELETE_WORD_RIGHT = 'workbench.action.terminal.deleteWordRight', + MOVE_TO_LINE_START = 'workbench.action.terminal.moveToLineStart', + MOVE_TO_LINE_END = 'workbench.action.terminal.moveToLineEnd', + NEW = 'workbench.action.terminal.new', + NEW_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.newInActiveWorkspace', + SPLIT = 'workbench.action.terminal.split', + SPLIT_IN_ACTIVE_WORKSPACE = 'workbench.action.terminal.splitInActiveWorkspace', + FOCUS_PREVIOUS_PANE = 'workbench.action.terminal.focusPreviousPane', + FOCUS_NEXT_PANE = 'workbench.action.terminal.focusNextPane', + RESIZE_PANE_LEFT = 'workbench.action.terminal.resizePaneLeft', + RESIZE_PANE_RIGHT = 'workbench.action.terminal.resizePaneRight', + RESIZE_PANE_UP = 'workbench.action.terminal.resizePaneUp', + RESIZE_PANE_DOWN = 'workbench.action.terminal.resizePaneDown', + FOCUS = 'workbench.action.terminal.focus', + FOCUS_NEXT = 'workbench.action.terminal.focusNext', + FOCUS_PREVIOUS = 'workbench.action.terminal.focusPrevious', + PASTE = 'workbench.action.terminal.paste', + SELECT_DEFAULT_SHELL = 'workbench.action.terminal.selectDefaultShell', + RUN_SELECTED_TEXT = 'workbench.action.terminal.runSelectedText', + RUN_ACTIVE_FILE = 'workbench.action.terminal.runActiveFile', + SWITCH_TERMINAL = 'workbench.action.terminal.switchTerminal', + SCROLL_DOWN_LINE = 'workbench.action.terminal.scrollDown', + SCROLL_DOWN_PAGE = 'workbench.action.terminal.scrollDownPage', + SCROLL_TO_BOTTOM = 'workbench.action.terminal.scrollToBottom', + SCROLL_UP_LINE = 'workbench.action.terminal.scrollUp', + SCROLL_UP_PAGE = 'workbench.action.terminal.scrollUpPage', + SCROLL_TO_TOP = 'workbench.action.terminal.scrollToTop', + CLEAR = 'workbench.action.terminal.clear', + CLEAR_SELECTION = 'workbench.action.terminal.clearSelection', + WORKSPACE_SHELL_ALLOW = 'workbench.action.terminal.allowWorkspaceShell', + WORKSPACE_SHELL_DISALLOW = 'workbench.action.terminal.disallowWorkspaceShell', + RENAME = 'workbench.action.terminal.rename', + FIND_WIDGET_FOCUS = 'workbench.action.terminal.focusFindWidget', + FIND_WIDGET_HIDE = 'workbench.action.terminal.hideFindWidget', + QUICK_OPEN_TERM = 'workbench.action.quickOpenTerm', + SCROLL_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.scrollToPreviousCommand', + SCROLL_TO_NEXT_COMMAND = 'workbench.action.terminal.scrollToNextCommand', + SELECT_TO_PREVIOUS_COMMAND = 'workbench.action.terminal.selectToPreviousCommand', + SELECT_TO_NEXT_COMMAND = 'workbench.action.terminal.selectToNextCommand', + SELECT_TO_PREVIOUS_LINE = 'workbench.action.terminal.selectToPreviousLine', + SELECT_TO_NEXT_LINE = 'workbench.action.terminal.selectToNextLine', + TOGGLE_ESCAPE_SEQUENCE_LOGGING = 'toggleEscapeSequenceLogging', + SEND_SEQUENCE = 'workbench.action.terminal.sendSequence' +} + + +export function setupTerminalCommands(): void { registerOpenTerminalAtIndexCommands(); } @@ -17,7 +70,7 @@ function registerOpenTerminalAtIndexCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: `workbench.action.terminal.focusAtIndex${visibleIndex}`, - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: void 0, primary: null, handler: accessor => { @@ -27,4 +80,4 @@ function registerOpenTerminalAtIndexCommands(): void { } }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/terminal/common/terminalMenu.ts b/src/vs/workbench/parts/terminal/common/terminalMenu.ts new file mode 100644 index 00000000000..873c8c688a5 --- /dev/null +++ b/src/vs/workbench/parts/terminal/common/terminalMenu.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; + +export function setupTerminalMenu() { + + // View menu + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_panels', + command: { + id: TERMINAL_COMMAND_ID.TOGGLE, + title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal") + }, + order: 3 + }); + + // Manage + const createGroup = '1_create'; + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: createGroup, + command: { + id: TERMINAL_COMMAND_ID.NEW, + title: nls.localize({ key: 'miNewTerminal', comment: ['&& denotes a mnemonic'] }, "&&New Terminal") + }, + order: 1 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: createGroup, + command: { + id: TERMINAL_COMMAND_ID.SPLIT, + title: nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), + precondition: ContextKeyExpr.has('terminalIsOpen') + }, + order: 2 + }); + + // Run + const runGroup = '2_run'; + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: runGroup, + command: { + id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, + title: nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File") + }, + order: 3 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { + group: runGroup, + command: { + id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, + title: nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text") + }, + order: 4 + }); +} diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index e294388516b..d0c505f4eda 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -3,17 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as errors from 'vs/base/common/errors'; import { Event, Emitter } from 'vs/base/common/event'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; - -const TERMINAL_STATE_STORAGE_KEY = 'terminal.state'; +import { IStorageService } from 'vs/platform/storage/common/storage'; export abstract class TerminalService implements ITerminalService { public _serviceBrand: any; @@ -41,10 +38,14 @@ export abstract class TerminalService implements ITerminalService { public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } protected readonly _onInstanceRequestExtHostProcess: Emitter = new Emitter(); public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.event; } + protected readonly _onInstanceDimensionsChanged: Emitter = new Emitter(); + public get onInstanceDimensionsChanged(): Event { return this._onInstanceDimensionsChanged.event; } protected readonly _onInstancesChanged: Emitter = new Emitter(); public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } protected readonly _onInstanceTitleChanged: Emitter = new Emitter(); public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + protected readonly _onActiveInstanceChanged: Emitter = new Emitter(); + public get onActiveInstanceChanged(): Event { return this._onActiveInstanceChanged.event; } protected readonly _onTabDisposed: Emitter = new Emitter(); public get onTabDisposed(): Event { return this._onTabDisposed.event; } @@ -66,40 +67,28 @@ export abstract class TerminalService implements ITerminalService { this._findWidgetVisible = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); this.onTabDisposed(tab => this._removeTab(tab)); - lifecycleService.when(LifecyclePhase.Restoring).then(() => this._restoreTabs()); + this._handleContextKeys(); + } + + private _handleContextKeys(): void { + const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); + + const updateTerminalContextKeys = () => { + terminalIsOpenContext.set(this.terminalInstances.length > 0); + }; + + this.onInstancesChanged(() => updateTerminalContextKeys()); } protected abstract _showTerminalCloseConfirmation(): TPromise; public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + public abstract createTerminalRenderer(name: string): ITerminalInstance; public abstract createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance; public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; public abstract selectDefaultWindowsShell(): TPromise; public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; public abstract requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; - private _restoreTabs(): void { - if (!this.configHelper.config.experimentalRestore) { - return; - } - - const tabConfigsJson = this._storageService.get(TERMINAL_STATE_STORAGE_KEY, StorageScope.WORKSPACE); - if (!tabConfigsJson) { - return; - } - - const tabConfigs = <{ instances: IShellLaunchConfig[] }[]>JSON.parse(tabConfigsJson); - if (!Array.isArray(tabConfigs)) { - return; - } - - tabConfigs.forEach(tabConfig => { - const instance = this.createTerminal(tabConfig.instances[0]); - for (let i = 1; i < tabConfig.instances.length; i++) { - this.splitInstance(instance, tabConfig.instances[i]); - } - }); - } - private _onWillShutdown(): boolean | TPromise { if (this.terminalInstances.length === 0) { // No terminal instances, don't veto @@ -122,22 +111,12 @@ export abstract class TerminalService implements ITerminalService { } private _onShutdown(): void { - // Store terminal tab layout - if (this.configHelper.config.experimentalRestore) { - const configs = this.terminalTabs.map(tab => { - return { - instances: tab.terminalInstances.map(instance => instance.shellLaunchConfig) - }; - }); - this._storageService.store(TERMINAL_STATE_STORAGE_KEY, JSON.stringify(configs), StorageScope.WORKSPACE); - } - // Dispose of all instances - this.terminalInstances.forEach(instance => instance.dispose()); + this.terminalInstances.forEach(instance => instance.dispose(true)); } public getTabLabels(): string[] { - return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title}`); + return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`); } private _removeTab(tab: ITerminalTab): void { @@ -152,7 +131,7 @@ export abstract class TerminalService implements ITerminalService { if (wasActiveTab && this._terminalTabs.length > 0) { // TODO: Only focus the new tab if the removed tab had focus? // const hasFocusOnExit = tab.activeInstance.hadFocusOnExit; - let newIndex = index < this._terminalTabs.length ? index : this._terminalTabs.length - 1; + const newIndex = index < this._terminalTabs.length ? index : this._terminalTabs.length - 1; this.setActiveTabByIndex(newIndex); this.getActiveInstance().focus(true); } @@ -162,6 +141,7 @@ export abstract class TerminalService implements ITerminalService { // launch. if (this._terminalTabs.length === 0 && !this._isShuttingDown) { this.hidePanel(); + this._onActiveInstanceChanged.fire(undefined); } // Fire events @@ -287,6 +267,8 @@ export abstract class TerminalService implements ITerminalService { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); instance.addDisposable(instance.onProcessIdReady(this._onInstanceProcessIdReady.fire, this._onInstanceProcessIdReady)); + instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance))); + instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); } private _getTabForInstance(instance: ITerminalInstance): ITerminalTab { @@ -301,7 +283,7 @@ export abstract class TerminalService implements ITerminalService { public showPanel(focus?: boolean): TPromise { return new TPromise((complete) => { - let panel = this._panelService.getActivePanel(); + const panel = this._panelService.getActivePanel(); if (!panel || panel.getId() !== TERMINAL_PANEL_ID) { return this._panelService.openPanel(TERMINAL_PANEL_ID, focus).then(() => { if (focus) { @@ -310,11 +292,14 @@ export abstract class TerminalService implements ITerminalService { setTimeout(() => { const instance = this.getActiveInstance(); if (instance) { - instance.focus(true); + instance.focusWhenReady(true).then(() => complete(void 0)); + } else { + complete(void 0); } }, 0); + } else { + complete(void 0); } - complete(void 0); }); } else { if (focus) { @@ -323,11 +308,14 @@ export abstract class TerminalService implements ITerminalService { setTimeout(() => { const instance = this.getActiveInstance(); if (instance) { - instance.focus(true); + instance.focusWhenReady(true).then(() => complete(void 0)); + } else { + complete(void 0); } }, 0); + } else { + complete(void 0); } - complete(void 0); } return undefined; }); @@ -336,14 +324,12 @@ export abstract class TerminalService implements ITerminalService { public hidePanel(): void { const panel = this._panelService.getActivePanel(); if (panel && panel.getId() === TERMINAL_PANEL_ID) { - this._partService.setPanelHidden(true).done(undefined, errors.onUnexpectedError); + this._partService.setPanelHidden(true); } } public abstract focusFindWidget(): TPromise; public abstract hideFindWidget(): void; - public abstract showNextFindTermFindWidget(): void; - public abstract showPreviousFindTermFindWidget(): void; private _getIndexFromId(terminalId: number): number { let terminalIndex = -1; diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/scrollbar.css b/src/vs/workbench/parts/terminal/electron-browser/media/scrollbar.css index 55ccd4c3e6c..0d28e3564d1 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/scrollbar.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/scrollbar.css @@ -24,6 +24,7 @@ background-color: inherit; } +.monaco-workbench .panel.integrated-terminal .find-focused .xterm .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm.focus .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm:focus .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm:hover .xterm-viewport { diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css index 42d4a6496db..c5da665535e 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.css @@ -17,6 +17,7 @@ height: 100%; width: 100%; box-sizing: border-box; + overflow: hidden; } .monaco-workbench .panel.integrated-terminal .terminal-tab { diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css index 95d45b4670b..be7a3c7a177 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css @@ -162,4 +162,9 @@ .xterm-cursor-pointer { cursor: pointer !important; -} \ No newline at end of file +} + +.xterm.xterm-cursor-crosshair { + /* Column selection mode */ + cursor: crosshair !important; +} 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 ba97ea5f903..3c5ed98fa83 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -11,21 +11,20 @@ import * as debugActions from 'vs/workbench/parts/debug/browser/debugActions'; import * as nls from 'vs/nls'; import * as panel from 'vs/workbench/browser/panel'; import * as platform from 'vs/base/common/platform'; -import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, DEFAULT_LINE_HEIGHT, DEFAULT_LETTER_SPACING } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, DEFAULT_LINE_HEIGHT, DEFAULT_LETTER_SPACING } from 'vs/workbench/parts/terminal/common/terminal'; import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KillTerminalAction, ClearSelectionTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, SplitInActiveWorkspaceTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, ScrollToPreviousCommandAction, ScrollToNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction, SelectToPreviousLineAction, SelectToNextLineAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; +import { KillTerminalAction, ClearSelectionTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, SplitInActiveWorkspaceTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, ScrollToPreviousCommandAction, ScrollToNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction, SelectToPreviousLineAction, SelectToNextLineAction, ToggleEscapeSequenceLoggingAction, SendSequenceTerminalCommand } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { TerminalService } from 'vs/workbench/parts/terminal/electron-browser/terminalService'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { OpenNextRecentlyUsedEditorInGroupAction, OpenPreviousRecentlyUsedEditorInGroupAction, FocusActiveGroupAction, FocusFirstGroupAction, FocusLastGroupAction, OpenFirstEditorInGroup, OpenLastEditorInGroup } from 'vs/workbench/browser/parts/editor/editorActions'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { registerColors } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; @@ -37,6 +36,8 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; import { TerminalPickerHandler } from 'vs/workbench/parts/terminal/browser/terminalQuickOpen'; +import { setupTerminalCommands, TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import { setupTerminalMenu } from 'vs/workbench/parts/terminal/common/terminalMenu'; const quickOpenRegistry = (Registry.as(QuickOpenExtensions.Quickopen)); @@ -66,68 +67,87 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenTermAction, Q const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionTermContributor); -let configurationRegistry = Registry.as(Extensions.Configuration); +const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ - 'id': 'terminal', - 'order': 100, - 'title': nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), - 'type': 'object', - 'properties': { + id: 'terminal', + order: 100, + title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), + type: 'object', + properties: { 'terminal.integrated.shell.linux': { - 'description': nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux."), - 'type': 'string', - 'default': getTerminalDefaultShellUnixLike() + markdownDescription: nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'string', + default: getTerminalDefaultShellUnixLike() }, 'terminal.integrated.shellArgs.linux': { - 'description': nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal."), - 'type': 'array', - 'items': { - 'type': 'string' + markdownDescription: nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' }, - 'default': [] + default: [] }, 'terminal.integrated.shell.osx': { - 'description': nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on OS X."), - 'type': 'string', - 'default': getTerminalDefaultShellUnixLike() + markdownDescription: nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'string', + default: getTerminalDefaultShellUnixLike() }, 'terminal.integrated.shellArgs.osx': { - 'description': nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the OS X terminal."), - 'type': 'array', - 'items': { - 'type': 'string' + markdownDescription: nls.localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'array', + items: { + type: 'string' }, // Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This // is the reason terminals on macOS typically run login shells by default which set up // the environment. See http://unix.stackexchange.com/a/119675/115410 - 'default': ['-l'] + default: ['-l'] }, 'terminal.integrated.shell.windows': { - 'description': nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. When using shells shipped with Windows (cmd, PowerShell or Bash on Ubuntu)."), - 'type': 'string', - 'default': getTerminalDefaultShellWindows() + markdownDescription: nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + type: 'string', + default: getTerminalDefaultShellWindows() }, 'terminal.integrated.shellArgs.windows': { - 'description': nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal."), - 'type': 'array', - 'items': { - 'type': 'string' - }, - 'default': [] + markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + 'anyOf': [ + { + type: 'array', + items: { + type: 'string', + markdownDescription: nls.localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + }, + }, + { + type: 'string', + markdownDescription: nls.localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + } + ], + default: [] }, 'terminal.integrated.macOptionIsMeta': { - 'description': nls.localize('terminal.integrated.macOptionIsMeta', "Treat the option key as the meta key in the terminal on macOS."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.macOptionIsMeta', "Controls whether to treat the option key as the meta key in the terminal on macOS."), + type: 'boolean', + default: false + }, + 'terminal.integrated.macOptionClickForcesSelection': { + description: nls.localize('terminal.integrated.macOptionClickForcesSelection', "Controls whether to force selection when using Option+click on macOS. This will force a regular (line) selection and disallow the use of column selection mode. This enables copying and pasting using the regular terminal selection, for example, when mouse mode is enabled in tmux."), + type: 'boolean', + default: false }, 'terminal.integrated.copyOnSelection': { - 'description': nls.localize('terminal.integrated.copyOnSelection', "When set, text selected in the terminal will be copied to the clipboard."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.copyOnSelection', "Controls whether text selected in the terminal will be copied to the clipboard."), + type: 'boolean', + default: false + }, + 'terminal.integrated.drawBoldTextInBrightColors': { + description: nls.localize('terminal.integrated.drawBoldTextInBrightColors', "Controls whether bold text in the terminal will always use the \"bright\" ANSI color variant."), + type: 'boolean', + default: true }, 'terminal.integrated.fontFamily': { - 'description': nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to editor.fontFamily's value."), - 'type': 'string' + markdownDescription: nls.localize('terminal.integrated.fontFamily', "Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."), + type: 'string' }, // TODO: Support font ligatures // 'terminal.integrated.fontLigatures': { @@ -136,97 +156,140 @@ configurationRegistry.registerConfiguration({ // 'default': false // }, 'terminal.integrated.fontSize': { - 'description': nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."), - 'type': 'number', - 'default': EDITOR_FONT_DEFAULTS.fontSize + description: nls.localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."), + type: 'number', + default: EDITOR_FONT_DEFAULTS.fontSize }, 'terminal.integrated.letterSpacing': { - 'description': nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."), - 'type': 'number', - 'default': DEFAULT_LETTER_SPACING + description: nls.localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."), + type: 'number', + default: DEFAULT_LETTER_SPACING }, 'terminal.integrated.lineHeight': { - 'description': nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."), - 'type': 'number', - 'default': DEFAULT_LINE_HEIGHT + description: nls.localize('terminal.integrated.lineHeight', "Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels."), + type: 'number', + default: DEFAULT_LINE_HEIGHT }, 'terminal.integrated.fontWeight': { - 'type': 'string', - 'enum': ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - 'description': nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."), - 'default': 'normal' + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: nls.localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."), + default: 'normal' }, 'terminal.integrated.fontWeightBold': { - 'type': 'string', - 'enum': ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - 'description': nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."), - 'default': 'bold' + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: nls.localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."), + default: 'bold' }, 'terminal.integrated.cursorBlinking': { - 'description': nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.cursorBlinking', "Controls whether the terminal cursor blinks."), + type: 'boolean', + default: false }, 'terminal.integrated.cursorStyle': { - 'description': nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."), - 'enum': [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], - 'default': TerminalCursorStyle.BLOCK + description: nls.localize('terminal.integrated.cursorStyle', "Controls the style of terminal cursor."), + enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], + default: TerminalCursorStyle.BLOCK }, 'terminal.integrated.scrollback': { - 'description': nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), - 'type': 'number', - 'default': 1000 + description: nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), + type: 'number', + default: 1000 }, 'terminal.integrated.setLocaleVariables': { - 'description': nls.localize('terminal.integrated.setLocaleVariables', "Controls whether locale variables are set at startup of the terminal, this defaults to true on OS X, false on other platforms."), - 'type': 'boolean', - 'default': platform.isMacintosh + markdownDescription: nls.localize('terminal.integrated.setLocaleVariables', "Controls whether locale variables are set at startup of the terminal, this defaults to `true` on macOS, `false` on other platforms."), + type: 'boolean', + default: platform.isMacintosh }, 'terminal.integrated.rendererType': { - 'type': 'string', - 'enum': ['auto', 'canvas', 'dom'], + type: 'string', + enum: ['auto', 'canvas', 'dom'], + enumDescriptions: [ + nls.localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."), + nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer"), + nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer.") + ], default: 'auto', - description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered, the options are \"canvas\" for the standard (fast) canvas renderer, \"dom\" for the fallback DOM-based renderer or \"auto\" which lets VS Code guess which will be best. This setting needs VS Code to reload in order to take effect.") + description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered.") }, 'terminal.integrated.rightClickBehavior': { - 'type': 'string', - 'enum': ['default', 'copyPaste', 'selectWord'], + type: 'string', + enum: ['default', 'copyPaste', 'selectWord'], + enumDescriptions: [ + nls.localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."), + nls.localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."), + nls.localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.") + ], default: platform.isMacintosh ? 'selectWord' : platform.isWindows ? 'copyPaste' : 'default', - description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click, possibilities are \"default\", \"copyPaste\", and \"selectWord\". \"default\" will show the context menu, \"copyPaste\" will copy when there is a selection otherwise paste, \"selectWord\" will select the word under the cursor and show the context menu.") + description: nls.localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.") }, 'terminal.integrated.cwd': { - 'description': nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."), - 'type': 'string', - 'default': undefined + description: nls.localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."), + type: 'string', + default: undefined }, 'terminal.integrated.confirmOnExit': { - 'description': nls.localize('terminal.integrated.confirmOnExit', "Whether to confirm on exit if there are active terminal sessions."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.confirmOnExit', "Controls whether to confirm on exit if there are active terminal sessions."), + type: 'boolean', + default: false }, 'terminal.integrated.enableBell': { - 'description': nls.localize('terminal.integrated.enableBell', "Whether the terminal bell is enabled or not."), - 'type': 'boolean', - 'default': false + description: nls.localize('terminal.integrated.enableBell', "Controls whether the terminal bell is enabled."), + type: 'boolean', + default: false }, 'terminal.integrated.commandsToSkipShell': { - 'description': nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open."), - 'type': 'array', - 'items': { - 'type': 'string' + description: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open."), + type: 'array', + items: { + type: 'string' }, - 'default': [ + default: [ + TERMINAL_COMMAND_ID.CLEAR_SELECTION, + TERMINAL_COMMAND_ID.CLEAR, + TERMINAL_COMMAND_ID.COPY_SELECTION, + TERMINAL_COMMAND_ID.DELETE_WORD_LEFT, + TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT, + TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS, + TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE, + TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE, + TERMINAL_COMMAND_ID.FOCUS_NEXT, + TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE, + TERMINAL_COMMAND_ID.FOCUS_PREVIOUS, + TERMINAL_COMMAND_ID.FOCUS, + TERMINAL_COMMAND_ID.KILL, + TERMINAL_COMMAND_ID.MOVE_TO_LINE_END, + TERMINAL_COMMAND_ID.MOVE_TO_LINE_START, + TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE, + TERMINAL_COMMAND_ID.NEW, + TERMINAL_COMMAND_ID.PASTE, + TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN, + TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT, + TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT, + TERMINAL_COMMAND_ID.RESIZE_PANE_UP, + TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, + TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, + TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE, + TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE, + TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM, + TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND, + TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND, + TERMINAL_COMMAND_ID.SCROLL_TO_TOP, + TERMINAL_COMMAND_ID.SCROLL_UP_LINE, + TERMINAL_COMMAND_ID.SCROLL_UP_PAGE, + TERMINAL_COMMAND_ID.SELECT_ALL, + TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND, + TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE, + TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND, + TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE, + TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE, + TERMINAL_COMMAND_ID.SPLIT, + TERMINAL_COMMAND_ID.TOGGLE, ToggleTabFocusModeAction.ID, QUICKOPEN_ACTION_ID, QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, ShowAllCommandsAction.ID, - CreateNewTerminalAction.ID, - CreateNewInActiveWorkspaceTerminalAction.ID, - CopyTerminalSelectionAction.ID, - KillTerminalAction.ID, - FocusActiveTerminalAction.ID, - FocusPreviousTerminalAction.ID, - FocusNextTerminalAction.ID, 'workbench.action.tasks.build', 'workbench.action.tasks.restartTask', 'workbench.action.tasks.runTask', @@ -250,18 +313,6 @@ configurationRegistry.registerConfiguration({ 'workbench.action.focusSixthEditorGroup', 'workbench.action.focusSeventhEditorGroup', 'workbench.action.focusEighthEditorGroup', - TerminalPasteAction.ID, - RunSelectedTextInTerminalAction.ID, - RunActiveFileInTerminalAction.ID, - ToggleTerminalAction.ID, - ScrollDownTerminalAction.ID, - ScrollDownPageTerminalAction.ID, - ScrollToBottomTerminalAction.ID, - ScrollUpTerminalAction.ID, - ScrollUpPageTerminalAction.ID, - ScrollToTopTerminalAction.ID, - ClearTerminalAction.ID, - ClearSelectionTerminalAction.ID, debugActions.StartAction.ID, debugActions.StopAction.ID, debugActions.RunAction.ID, @@ -278,71 +329,43 @@ configurationRegistry.registerConfiguration({ FocusLastGroupAction.ID, OpenFirstEditorInGroup.ID, OpenLastEditorInGroup.ID, - SelectAllTerminalAction.ID, - FocusTerminalFindWidgetAction.ID, - HideTerminalFindWidgetAction.ID, - ShowPreviousFindTermTerminalFindWidgetAction.ID, - ShowNextFindTermTerminalFindWidgetAction.ID, NavigateUpAction.ID, NavigateDownAction.ID, NavigateRightAction.ID, NavigateLeftAction.ID, - DeleteWordLeftTerminalAction.ID, - DeleteWordRightTerminalAction.ID, - MoveToLineStartTerminalAction.ID, - MoveToLineEndTerminalAction.ID, TogglePanelAction.ID, - 'workbench.action.quickOpenView', - SplitTerminalAction.ID, - SplitInActiveWorkspaceTerminalAction.ID, - FocusPreviousPaneTerminalAction.ID, - FocusNextPaneTerminalAction.ID, - ResizePaneLeftTerminalAction.ID, - ResizePaneRightTerminalAction.ID, - ResizePaneUpTerminalAction.ID, - ResizePaneDownTerminalAction.ID, - ScrollToPreviousCommandAction.ID, - ScrollToNextCommandAction.ID, - SelectToPreviousCommandAction.ID, - SelectToNextCommandAction.ID, - SelectToPreviousLineAction.ID, - SelectToNextLineAction.ID + 'workbench.action.quickOpenView' ].sort() }, 'terminal.integrated.env.osx': { - 'description': nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on OS X"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + markdownDescription: nls.localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.env.linux': { - 'description': nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + markdownDescription: nls.localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.env.windows': { - 'description': nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows"), - 'type': 'object', - 'additionalProperties': { - 'type': ['string', 'null'] + markdownDescription: nls.localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable."), + type: 'object', + additionalProperties: { + type: ['string', 'null'] }, - 'default': {} + default: {} }, 'terminal.integrated.showExitAlert': { - 'description': nls.localize('terminal.integrated.showExitAlert', "Show alert `The terminal process terminated with exit code` when exit code is non-zero."), - 'type': 'boolean', - 'default': true - }, - 'terminal.integrated.experimentalRestore': { - 'description': nls.localize('terminal.integrated.experimentalRestore', "Whether to restore terminal sessions for the workspace automatically when launching VS Code. This is an experimental setting; it may be buggy and could change in the future."), - 'type': 'boolean', - 'default': false - }, + description: nls.localize('terminal.integrated.showExitAlert', "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero."), + type: 'boolean', + default: true + } } }); @@ -354,12 +377,12 @@ registerSingleton(ITerminalService, TerminalService); nls.localize('terminal', "Terminal"), 'terminal', 40, - ToggleTerminalAction.ID + TERMINAL_COMMAND_ID.TOGGLE )); // On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl const category = nls.localize('terminalCategory', "Terminal"); -let actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); +const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.LABEL), 'Terminal: Kill the Active Terminal Instance', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, @@ -372,7 +395,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewTermina actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearSelectionTerminalAction, ClearSelectionTerminalAction.ID, ClearSelectionTerminalAction.LABEL, { primary: KeyCode.Escape, linux: { primary: KeyCode.Escape } -}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE)), 'Terminal: Escape selection', category); +}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE)), 'Terminal: Escape selection', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewInActiveWorkspaceTerminalAction, CreateNewInActiveWorkspaceTerminalAction.ID, CreateNewInActiveWorkspaceTerminalAction.LABEL), 'Terminal: Create New Integrated Terminal (In Active Workspace)', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusActiveTerminalAction, FocusActiveTerminalAction.ID, FocusActiveTerminalAction.LABEL), 'Terminal: Focus Terminal', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextTerminalAction, FocusNextTerminalAction.ID, FocusNextTerminalAction.LABEL), 'Terminal: Focus Next Terminal', category); @@ -425,7 +448,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToTopTermi actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_K, linux: { primary: null } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingsRegistry.WEIGHT.workbenchContrib(1)), 'Terminal: Clear', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category); if (platform.isWindows) { actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectDefaultShellWindowsTerminalAction, SelectDefaultShellWindowsTerminalAction.ID, SelectDefaultShellWindowsTerminalAction.LABEL), 'Terminal: Select Default Shell', category); } @@ -439,12 +462,6 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(HideTerminalFind primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape] }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE)), 'Terminal: Hide Find Widget', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextFindTermTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction.ID, ShowNextFindTermTerminalFindWidgetAction.LABEL, { - primary: KeyMod.Alt | KeyCode.DownArrow -}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE)), 'Terminal: Show Next Find Term', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction.ID, ShowPreviousFindTermTerminalFindWidgetAction.LABEL, { - primary: KeyMod.Alt | KeyCode.UpArrow -}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE)), 'Terminal: Show Previous Find Term', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DeleteWordLeftTerminalAction, DeleteWordLeftTerminalAction.ID, DeleteWordLeftTerminalAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.Backspace, mac: { primary: KeyMod.Alt | KeyCode.Backspace } @@ -524,7 +541,15 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextComm }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Next Command', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousLineAction, SelectToPreviousLineAction.ID, SelectToPreviousLineAction.LABEL), 'Terminal: Select To Previous Line', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextLineAction, SelectToNextLineAction.ID, SelectToNextLineAction.LABEL), 'Terminal: Select To Next Line', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEscapeSequenceLoggingAction, ToggleEscapeSequenceLoggingAction.ID, ToggleEscapeSequenceLoggingAction.LABEL), 'Terminal: Toggle Escape Sequence Logging', category); -terminalCommands.setup(); +const sendSequenceTerminalCommand = new SendSequenceTerminalCommand({ + id: SendSequenceTerminalCommand.ID, + precondition: null +}); +sendSequenceTerminalCommand.register(); + +setupTerminalCommands(); +setupTerminalMenu(); registerColors(); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 66c7c67b786..c313c0dd02a 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -16,22 +16,25 @@ import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IQuickOpenService, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; +import { IQuickInputService, IPickOptions, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ActionBarContributor } from 'vs/workbench/browser/actions'; import { TerminalEntry } from 'vs/workbench/parts/terminal/browser/terminalQuickOpen'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import { Command } from 'vs/editor/browser/editorExtensions'; +import { timeout } from 'vs/base/common/async'; export const TERMINAL_PICKER_PREFIX = 'term '; export class ToggleTerminalAction extends TogglePanelAction { - public static readonly ID = 'workbench.action.terminal.toggleTerminal'; + public static readonly ID = TERMINAL_COMMAND_ID.TOGGLE; public static readonly LABEL = nls.localize('workbench.action.terminal.toggleTerminal', "Toggle Integrated Terminal"); constructor( @@ -59,7 +62,7 @@ export class ToggleTerminalAction extends TogglePanelAction { export class KillTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.kill'; + public static readonly ID = TERMINAL_COMMAND_ID.KILL; public static readonly LABEL = nls.localize('workbench.action.terminal.kill', "Kill the Active Terminal Instance"); public static readonly PANEL_LABEL = nls.localize('workbench.action.terminal.kill.short', "Kill Terminal"); @@ -71,9 +74,9 @@ export class KillTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); - if (terminalInstance) { - this.terminalService.getActiveInstance().dispose(); + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.dispose(); if (this.terminalService.terminalInstances.length > 0) { this.terminalService.showPanel(true); } @@ -84,7 +87,7 @@ export class KillTerminalAction extends Action { export class QuickKillTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.quickKill'; + public static readonly ID = TERMINAL_COMMAND_ID.QUICK_KILL; public static readonly LABEL = nls.localize('workbench.action.terminal.quickKill', "Kill Terminal Instance"); constructor( @@ -100,7 +103,7 @@ export class QuickKillTerminalAction extends Action { if (instance) { instance.dispose(); } - return TPromise.timeout(50).then(result => this.quickOpenService.show(TERMINAL_PICKER_PREFIX, null)); + return TPromise.wrap(timeout(50)).then(result => this.quickOpenService.show(TERMINAL_PICKER_PREFIX, null)); } } @@ -110,8 +113,9 @@ export class QuickKillTerminalAction extends Action { */ export class CopyTerminalSelectionAction extends Action { - public static readonly ID = 'workbench.action.terminal.copySelection'; + public static readonly ID = TERMINAL_COMMAND_ID.COPY_SELECTION; public static readonly LABEL = nls.localize('workbench.action.terminal.copySelection', "Copy Selection"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.copySelection.short', "Copy"); constructor( id: string, label: string, @@ -121,7 +125,7 @@ export class CopyTerminalSelectionAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.copySelection(); } @@ -131,7 +135,7 @@ export class CopyTerminalSelectionAction extends Action { export class SelectAllTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectAll'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_ALL; public static readonly LABEL = nls.localize('workbench.action.terminal.selectAll', "Select All"); constructor( @@ -142,7 +146,7 @@ export class SelectAllTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.selectAll(); } @@ -161,7 +165,7 @@ export abstract class BaseSendTextTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this._terminalService.getActiveInstance(); + const terminalInstance = this._terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.sendText(this._text, false); } @@ -170,7 +174,7 @@ export abstract class BaseSendTextTerminalAction extends Action { } export class DeleteWordLeftTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.deleteWordLeft'; + public static readonly ID = TERMINAL_COMMAND_ID.DELETE_WORD_LEFT; public static readonly LABEL = nls.localize('workbench.action.terminal.deleteWordLeft', "Delete Word Left"); constructor( @@ -184,7 +188,7 @@ export class DeleteWordLeftTerminalAction extends BaseSendTextTerminalAction { } export class DeleteWordRightTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.deleteWordRight'; + public static readonly ID = TERMINAL_COMMAND_ID.DELETE_WORD_RIGHT; public static readonly LABEL = nls.localize('workbench.action.terminal.deleteWordRight', "Delete Word Right"); constructor( @@ -198,7 +202,7 @@ export class DeleteWordRightTerminalAction extends BaseSendTextTerminalAction { } export class MoveToLineStartTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.moveToLineStart'; + public static readonly ID = TERMINAL_COMMAND_ID.MOVE_TO_LINE_START; public static readonly LABEL = nls.localize('workbench.action.terminal.moveToLineStart', "Move To Line Start"); constructor( @@ -212,7 +216,7 @@ export class MoveToLineStartTerminalAction extends BaseSendTextTerminalAction { } export class MoveToLineEndTerminalAction extends BaseSendTextTerminalAction { - public static readonly ID = 'workbench.action.terminal.moveToLineEnd'; + public static readonly ID = TERMINAL_COMMAND_ID.MOVE_TO_LINE_END; public static readonly LABEL = nls.localize('workbench.action.terminal.moveToLineEnd', "Move To Line End"); constructor( @@ -225,11 +229,24 @@ export class MoveToLineEndTerminalAction extends BaseSendTextTerminalAction { } } +export class SendSequenceTerminalCommand extends Command { + public static readonly ID = TERMINAL_COMMAND_ID.SEND_SEQUENCE; + public static readonly LABEL = nls.localize('workbench.action.terminal.sendSequence', "Send Custom Sequence To Terminal"); + + public runCommand(accessor: ServicesAccessor, args: any): void { + const terminalInstance = accessor.get(ITerminalService).getActiveInstance(); + if (!terminalInstance) { + return; + } + terminalInstance.sendText(args.text, false); + } +} + export class CreateNewTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.new'; + public static readonly ID = TERMINAL_COMMAND_ID.NEW; public static readonly LABEL = nls.localize('workbench.action.terminal.new', "Create New Integrated Terminal"); - public static readonly PANEL_LABEL = nls.localize('workbench.action.terminal.new.short', "New Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.new.short', "New Terminal"); constructor( id: string, label: string, @@ -256,7 +273,7 @@ export class CreateNewTerminalAction extends Action { // single root instancePromise = TPromise.as(this.terminalService.createTerminal(undefined, true)); } else { - const options: IPickOptions = { + const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") }; instancePromise = this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]).then(workspace => { @@ -280,7 +297,7 @@ export class CreateNewTerminalAction extends Action { export class CreateNewInActiveWorkspaceTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.newInActiveWorkspace'; + public static readonly ID = TERMINAL_COMMAND_ID.NEW_IN_ACTIVE_WORKSPACE; public static readonly LABEL = nls.localize('workbench.action.terminal.newInActiveWorkspace', "Create New Integrated Terminal (In Active Workspace)"); constructor( @@ -301,8 +318,9 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action { } export class SplitTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.split'; + public static readonly ID = TERMINAL_COMMAND_ID.SPLIT; public static readonly LABEL = nls.localize('workbench.action.terminal.split', "Split Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.split.short', "Split"); constructor( id: string, label: string, @@ -324,7 +342,7 @@ export class SplitTerminalAction extends Action { let pathPromise: TPromise = TPromise.as({}); if (folders.length > 1) { // Only choose a path when there's more than 1 folder - const options: IPickOptions = { + const options: IPickOptions = { placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") }; pathPromise = this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]).then(workspace => { @@ -347,7 +365,7 @@ export class SplitTerminalAction extends Action { } export class SplitInActiveWorkspaceTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.splitInActiveWorkspace'; + public static readonly ID = TERMINAL_COMMAND_ID.SPLIT_IN_ACTIVE_WORKSPACE; public static readonly LABEL = nls.localize('workbench.action.terminal.splitInActiveWorkspace', "Split Terminal (In Active Workspace)"); constructor( @@ -368,7 +386,7 @@ export class SplitInActiveWorkspaceTerminalAction extends Action { } export class FocusPreviousPaneTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusPreviousPane'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_PREVIOUS_PANE; public static readonly LABEL = nls.localize('workbench.action.terminal.focusPreviousPane', "Focus Previous Pane"); constructor( @@ -389,7 +407,7 @@ export class FocusPreviousPaneTerminalAction extends Action { } export class FocusNextPaneTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusNextPane'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_NEXT_PANE; public static readonly LABEL = nls.localize('workbench.action.terminal.focusNextPane', "Focus Next Pane"); constructor( @@ -428,7 +446,7 @@ export abstract class BaseFocusDirectionTerminalAction extends Action { } export class ResizePaneLeftTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneLeft'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_LEFT; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneLeft', "Resize Pane Left"); constructor( @@ -440,7 +458,7 @@ export class ResizePaneLeftTerminalAction extends BaseFocusDirectionTerminalActi } export class ResizePaneRightTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneRight'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_RIGHT; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneRight', "Resize Pane Right"); constructor( @@ -452,7 +470,7 @@ export class ResizePaneRightTerminalAction extends BaseFocusDirectionTerminalAct } export class ResizePaneUpTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneUp'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_UP; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneUp', "Resize Pane Up"); constructor( @@ -464,7 +482,7 @@ export class ResizePaneUpTerminalAction extends BaseFocusDirectionTerminalAction } export class ResizePaneDownTerminalAction extends BaseFocusDirectionTerminalAction { - public static readonly ID = 'workbench.action.terminal.resizePaneDown'; + public static readonly ID = TERMINAL_COMMAND_ID.RESIZE_PANE_DOWN; public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneDown', "Resize Pane Down"); constructor( @@ -477,7 +495,7 @@ export class ResizePaneDownTerminalAction extends BaseFocusDirectionTerminalActi export class FocusActiveTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focus'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focus', "Focus Terminal"); constructor( @@ -499,7 +517,7 @@ export class FocusActiveTerminalAction extends Action { export class FocusNextTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusNext'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_NEXT; public static readonly LABEL = nls.localize('workbench.action.terminal.focusNext', "Focus Next Terminal"); constructor( @@ -517,7 +535,7 @@ export class FocusNextTerminalAction extends Action { export class FocusPreviousTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusPrevious'; + public static readonly ID = TERMINAL_COMMAND_ID.FOCUS_PREVIOUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focusPrevious', "Focus Previous Terminal"); constructor( @@ -535,8 +553,9 @@ export class FocusPreviousTerminalAction extends Action { export class TerminalPasteAction extends Action { - public static readonly ID = 'workbench.action.terminal.paste'; + public static readonly ID = TERMINAL_COMMAND_ID.PASTE; public static readonly LABEL = nls.localize('workbench.action.terminal.paste', "Paste into Active Terminal"); + public static readonly SHORT_LABEL = nls.localize('workbench.action.terminal.paste.short', "Paste"); constructor( id: string, label: string, @@ -556,8 +575,8 @@ export class TerminalPasteAction extends Action { export class SelectDefaultShellWindowsTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectDefaultShell'; - public static readonly LABEL = nls.localize('workbench.action.terminal.DefaultShell', "Select Default Shell"); + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_DEFAULT_SHELL; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectDefaultShell', "Select Default Shell"); constructor( id: string, label: string, @@ -573,7 +592,7 @@ export class SelectDefaultShellWindowsTerminalAction extends Action { export class RunSelectedTextInTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.runSelectedText'; + public static readonly ID = TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT; public static readonly LABEL = nls.localize('workbench.action.terminal.runSelectedText', "Run Selected Text In Active Terminal"); constructor( @@ -598,7 +617,7 @@ export class RunSelectedTextInTerminalAction extends Action { if (selection.isEmpty()) { text = editor.getModel().getLineContent(selection.selectionStartLineNumber).trim(); } else { - let endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF; + const endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF; text = editor.getModel().getValueInRange(selection, endOfLinePreference); } instance.sendText(text, true); @@ -608,7 +627,7 @@ export class RunSelectedTextInTerminalAction extends Action { export class RunActiveFileInTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.runActiveFile'; + public static readonly ID = TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE; public static readonly LABEL = nls.localize('workbench.action.terminal.runActiveFile', "Run Active File In Active Terminal"); constructor( @@ -625,7 +644,7 @@ export class RunActiveFileInTerminalAction extends Action { if (!instance) { return TPromise.as(void 0); } - const editor = this.codeEditorService.getFocusedCodeEditor(); + const editor = this.codeEditorService.getActiveCodeEditor(); if (!editor) { return TPromise.as(void 0); } @@ -641,14 +660,14 @@ export class RunActiveFileInTerminalAction extends Action { export class SwitchTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.switchTerminal'; + public static readonly ID = TERMINAL_COMMAND_ID.SWITCH_TERMINAL; public static readonly LABEL = nls.localize('workbench.action.terminal.switchTerminal', "Switch Terminal"); constructor( id: string, label: string, @ITerminalService private terminalService: ITerminalService ) { - super(SwitchTerminalAction.ID, SwitchTerminalAction.LABEL, 'terminal-action switch-terminal'); + super(id, label, 'terminal-action switch-terminal'); } public run(item?: string): TPromise { @@ -669,7 +688,7 @@ export class SwitchTerminalActionItem extends SelectActionItem { @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, terminalService.getTabLabels(), terminalService.activeTabIndex, contextViewService); + super(null, action, terminalService.getTabLabels(), terminalService.activeTabIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Terminals') }); this.toDispose.push(terminalService.onInstancesChanged(this._updateItems, this)); this.toDispose.push(terminalService.onActiveTabChanged(this._updateItems, this)); @@ -684,7 +703,7 @@ export class SwitchTerminalActionItem extends SelectActionItem { export class ScrollDownTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollDown'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_DOWN_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollDown', "Scroll Down (Line)"); constructor( @@ -695,7 +714,7 @@ export class ScrollDownTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollDownLine(); } @@ -705,7 +724,7 @@ export class ScrollDownTerminalAction extends Action { export class ScrollDownPageTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollDownPage'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_DOWN_PAGE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollDownPage', "Scroll Down (Page)"); constructor( @@ -716,7 +735,7 @@ export class ScrollDownPageTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollDownPage(); } @@ -726,7 +745,7 @@ export class ScrollDownPageTerminalAction extends Action { export class ScrollToBottomTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToBottom'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_BOTTOM; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToBottom', "Scroll to Bottom"); constructor( @@ -737,7 +756,7 @@ export class ScrollToBottomTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollToBottom(); } @@ -747,7 +766,7 @@ export class ScrollToBottomTerminalAction extends Action { export class ScrollUpTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollUp'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_UP_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollUp', "Scroll Up (Line)"); constructor( @@ -758,7 +777,7 @@ export class ScrollUpTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollUpLine(); } @@ -768,7 +787,7 @@ export class ScrollUpTerminalAction extends Action { export class ScrollUpPageTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollUpPage'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_UP_PAGE; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollUpPage', "Scroll Up (Page)"); constructor( @@ -779,7 +798,7 @@ export class ScrollUpPageTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollUpPage(); } @@ -789,7 +808,7 @@ export class ScrollUpPageTerminalAction extends Action { export class ScrollToTopTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToTop'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_TOP; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToTop', "Scroll to Top"); constructor( @@ -800,7 +819,7 @@ export class ScrollToTopTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollToTop(); } @@ -810,7 +829,7 @@ export class ScrollToTopTerminalAction extends Action { export class ClearTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.clear'; + public static readonly ID = TERMINAL_COMMAND_ID.CLEAR; public static readonly LABEL = nls.localize('workbench.action.terminal.clear', "Clear"); constructor( @@ -821,7 +840,7 @@ export class ClearTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.clear(); } @@ -831,7 +850,7 @@ export class ClearTerminalAction extends Action { export class ClearSelectionTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.clearSelection'; + public static readonly ID = TERMINAL_COMMAND_ID.CLEAR_SELECTION; public static readonly LABEL = nls.localize('workbench.action.terminal.clearSelection', "Clear Selection"); constructor( @@ -842,7 +861,7 @@ export class ClearSelectionTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance && terminalInstance.hasSelection()) { terminalInstance.clearSelection(); } @@ -852,7 +871,7 @@ export class ClearSelectionTerminalAction extends Action { export class AllowWorkspaceShellTerminalCommand extends Action { - public static readonly ID = 'workbench.action.terminal.allowWorkspaceShell'; + public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_ALLOW; public static readonly LABEL = nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration"); constructor( @@ -870,7 +889,7 @@ export class AllowWorkspaceShellTerminalCommand extends Action { export class DisallowWorkspaceShellTerminalCommand extends Action { - public static readonly ID = 'workbench.action.terminal.disallowWorkspaceShell'; + public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_DISALLOW; public static readonly LABEL = nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration"); constructor( @@ -888,7 +907,7 @@ export class DisallowWorkspaceShellTerminalCommand extends Action { export class RenameTerminalAction extends Action { - public static readonly ID = 'workbench.action.terminal.rename'; + public static readonly ID = TERMINAL_COMMAND_ID.RENAME; public static readonly LABEL = nls.localize('workbench.action.terminal.rename', "Rename"); constructor( @@ -918,7 +937,7 @@ export class RenameTerminalAction extends Action { export class FocusTerminalFindWidgetAction extends Action { - public static readonly ID = 'workbench.action.terminal.focusFindWidget'; + public static readonly ID = TERMINAL_COMMAND_ID.FIND_WIDGET_FOCUS; public static readonly LABEL = nls.localize('workbench.action.terminal.focusFindWidget', "Focus Find Widget"); constructor( @@ -935,7 +954,7 @@ export class FocusTerminalFindWidgetAction extends Action { export class HideTerminalFindWidgetAction extends Action { - public static readonly ID = 'workbench.action.terminal.hideFindWidget'; + public static readonly ID = TERMINAL_COMMAND_ID.FIND_WIDGET_HIDE; public static readonly LABEL = nls.localize('workbench.action.terminal.hideFindWidget', "Hide Find Widget"); constructor( @@ -950,41 +969,6 @@ export class HideTerminalFindWidgetAction extends Action { } } -export class ShowNextFindTermTerminalFindWidgetAction extends Action { - - public static readonly ID = 'workbench.action.terminal.findWidget.history.showNext'; - public static readonly LABEL = nls.localize('nextTerminalFindTerm', "Show Next Find Term"); - - constructor( - id: string, label: string, - @ITerminalService private terminalService: ITerminalService - ) { - super(id, label); - } - - public run(): TPromise { - return TPromise.as(this.terminalService.showNextFindTermFindWidget()); - } -} - -export class ShowPreviousFindTermTerminalFindWidgetAction extends Action { - - public static readonly ID = 'workbench.action.terminal.findWidget.history.showPrevious'; - public static readonly LABEL = nls.localize('previousTerminalFindTerm', "Show Previous Find Term"); - - constructor( - id: string, label: string, - @ITerminalService private terminalService: ITerminalService - ) { - super(id, label); - } - - public run(): TPromise { - return TPromise.as(this.terminalService.showPreviousFindTermFindWidget()); - } -} - - export class QuickOpenActionTermContributor extends ActionBarContributor { constructor( @@ -994,7 +978,7 @@ export class QuickOpenActionTermContributor extends ActionBarContributor { } public getActions(context: any): IAction[] { - let actions: Action[] = []; + const actions: Action[] = []; if (context.element instanceof TerminalEntry) { actions.push(this.instantiationService.createInstance(RenameTerminalQuickOpenAction, RenameTerminalQuickOpenAction.ID, RenameTerminalQuickOpenAction.LABEL, context.element)); actions.push(this.instantiationService.createInstance(QuickKillTerminalAction, QuickKillTerminalAction.ID, QuickKillTerminalAction.LABEL, context.element)); @@ -1009,7 +993,7 @@ export class QuickOpenActionTermContributor extends ActionBarContributor { export class QuickOpenTermAction extends Action { - public static readonly ID = 'workbench.action.quickOpenTerm'; + public static readonly ID = TERMINAL_COMMAND_ID.QUICK_OPEN_TERM; public static readonly LABEL = nls.localize('quickOpenTerm', "Switch Active Terminal"); constructor( @@ -1041,14 +1025,14 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { public run(): TPromise { super.run(this.terminal) // This timeout is needed to make sure the previous quickOpen has time to close before we show the next one - .then(() => TPromise.timeout(50)) + .then(() => timeout(50)) .then(result => this.quickOpenService.show(TERMINAL_PICKER_PREFIX, null)); return TPromise.as(null); } } export class ScrollToPreviousCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToPreviousCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_PREVIOUS_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToPreviousCommand', "Scroll To Previous Command"); constructor( @@ -1069,7 +1053,7 @@ export class ScrollToPreviousCommandAction extends Action { } export class ScrollToNextCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.scrollToNextCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SCROLL_TO_NEXT_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.scrollToNextCommand', "Scroll To Next Command"); constructor( @@ -1090,7 +1074,7 @@ export class ScrollToNextCommandAction extends Action { } export class SelectToPreviousCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToPreviousCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousCommand', "Select To Previous Command"); constructor( @@ -1111,7 +1095,7 @@ export class SelectToPreviousCommandAction extends Action { } export class SelectToNextCommandAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToNextCommand'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_NEXT_COMMAND; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextCommand', "Select To Next Command"); constructor( @@ -1132,7 +1116,7 @@ export class SelectToNextCommandAction extends Action { } export class SelectToPreviousLineAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToPreviousLine'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"); constructor( @@ -1153,7 +1137,7 @@ export class SelectToPreviousLineAction extends Action { } export class SelectToNextLineAction extends Action { - public static readonly ID = 'workbench.action.terminal.selectToNextLine'; + public static readonly ID = TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE; public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"); constructor( @@ -1171,4 +1155,25 @@ export class SelectToNextLineAction extends Action { } return TPromise.as(void 0); } -} \ No newline at end of file +} + + +export class ToggleEscapeSequenceLoggingAction extends Action { + public static readonly ID = TERMINAL_COMMAND_ID.TOGGLE_ESCAPE_SEQUENCE_LOGGING; + public static readonly LABEL = nls.localize('workbench.action.terminal.toggleEscapeSequenceLogging', "Toggle Escape Sequence Logging"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.toggleEscapeSequenceLogging(); + } + return TPromise.as(void 0); + } +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index efbdc7e94fe..4d4f6645ad0 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -50,12 +50,12 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { public configFontIsMonospace(): boolean { this._createCharMeasureElementIfNecessary(); - let fontSize = 15; - let fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily; - let i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); - let w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); + const fontSize = 15; + const fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily; + const i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); + const w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); - let invalidBounds = !i_rect.width || !w_rect.width; + const invalidBounds = !i_rect.width || !w_rect.width; if (invalidBounds) { // There is no reason to believe the font is not Monospace. return true; @@ -88,7 +88,7 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont { this._createCharMeasureElementIfNecessary(); - let rect = this._getBoundingRectFor('X', fontFamily, fontSize); + const rect = this._getBoundingRectFor('X', fontFamily, fontSize); // Bounding client rect was invalid, use last font measurement if available. if (this._lastFontMeasurement && !rect.width && !rect.height) { @@ -122,7 +122,7 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { } } - let fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); + const fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); const letterSpacing = this.config.letterSpacing ? Math.max(Math.floor(this.config.letterSpacing), MINIMUM_LETTER_SPACING) : DEFAULT_LETTER_SPACING; const lineHeight = this.config.lineHeight ? Math.max(this.config.lineHeight, 1) : DEFAULT_LINE_HEIGHT; @@ -137,14 +137,14 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { // Get the character dimensions from xterm if it's available if (xterm) { - if (xterm.charMeasure && xterm.charMeasure.width && xterm.charMeasure.height) { + if (xterm._core.charMeasure && xterm._core.charMeasure.width && xterm._core.charMeasure.height) { return { fontFamily, fontSize, letterSpacing, lineHeight, - charHeight: xterm.charMeasure.height, - charWidth: xterm.charMeasure.width + charHeight: xterm._core.charMeasure.height, + charWidth: xterm._core.charMeasure.width }; } } @@ -157,9 +157,9 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, isAllowed, StorageScope.WORKSPACE); } - public mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig): void { + public mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride: platform.Platform = platform.platform): void { // Check whether there is a workspace setting - const platformKey = platform.isWindows ? 'windows' : platform.isMacintosh ? 'osx' : 'linux'; + const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux'; const shellConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.shell.${platformKey}`); const shellArgsConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.shellArgs.${platformKey}`); @@ -169,6 +169,11 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { isWorkspaceShellAllowed = this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, undefined); } + // Always allow [] args as it would lead to an odd error message and should not be dangerous + if (shellConfigValue.workspace === undefined && shellArgsConfigValue.workspace && shellArgsConfigValue.workspace.length === 0) { + isWorkspaceShellAllowed = true; + } + // Check if the value is neither blacklisted (false) or whitelisted (true) and ask for // permission if (isWorkspaceShellAllowed === undefined) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index a0ffa0e0c21..0caa75aa89a 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -14,7 +14,7 @@ import { Terminal as XTermTerminal } from 'vscode-xterm'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig, ITerminalProcessManager, ProcessState, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig, ITerminalProcessManager, ProcessState, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ITerminalDimensions } from 'vs/workbench/parts/terminal/common/terminal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; @@ -63,10 +63,9 @@ export class TerminalInstance implements ITerminalInstance { private _terminalHasTextContextKey: IContextKey; private _cols: number; private _rows: number; + private _dimensionsOverride: ITerminalDimensions; private _windowsShellHelper: WindowsShellHelper; - private _onLineDataListeners: ((lineData: string) => void)[]; - private _onDataListeners: ((data: string) => void)[]; - private _xtermReadyPromise: TPromise; + private _xtermReadyPromise: Promise; private _disposables: lifecycle.IDisposable[]; private _messageTitleDisposable: lifecycle.IDisposable; @@ -77,6 +76,8 @@ export class TerminalInstance implements ITerminalInstance { public disableLayout: boolean; public get id(): number { return this._id; } + public get cols(): number { return this._cols; } + public get rows(): number { return this._rows; } // TODO: Ideally processId would be merged into processReady public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; } // TODO: How does this work with detached processes? @@ -85,10 +86,11 @@ export class TerminalInstance implements ITerminalInstance { public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } - public get shellLaunchConfig(): IShellLaunchConfig { return Object.freeze(this._shellLaunchConfig); } + public get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; } public get commandTracker(): TerminalCommandTracker { return this._commandTracker; } - + private readonly _onExit: Emitter = new Emitter(); + public get onExit(): Event { return this._onExit.event; } private readonly _onDisposed: Emitter = new Emitter(); public get onDisposed(): Event { return this._onDisposed.event; } private readonly _onFocused: Emitter = new Emitter(); @@ -97,15 +99,24 @@ export class TerminalInstance implements ITerminalInstance { public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } private readonly _onTitleChanged: Emitter = new Emitter(); public get onTitleChanged(): Event { return this._onTitleChanged.event; } + private readonly _onData: Emitter = new Emitter(); + public get onData(): Event { return this._onData.event; } + private readonly _onLineData: Emitter = new Emitter(); + public get onLineData(): Event { return this._onLineData.event; } + private readonly _onRendererInput: Emitter = new Emitter(); + public get onRendererInput(): Event { return this._onRendererInput.event; } private readonly _onRequestExtHostProcess: Emitter = new Emitter(); public get onRequestExtHostProcess(): Event { return this._onRequestExtHostProcess.event; } + private readonly _onDimensionsChanged: Emitter = new Emitter(); + public get onDimensionsChanged(): Event { return this._onDimensionsChanged.event; } + private readonly _onFocus: Emitter = new Emitter(); + public get onFocus(): Event { return this._onFocus.event; } public constructor( - private _terminalFocusContextKey: IContextKey, - private _configHelper: TerminalConfigHelper, + private readonly _terminalFocusContextKey: IContextKey, + private readonly _configHelper: TerminalConfigHelper, private _container: HTMLElement, private _shellLaunchConfig: IShellLaunchConfig, - doCreateProcess: boolean, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, @@ -115,12 +126,10 @@ export class TerminalInstance implements ITerminalInstance { @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private _logService: ILogService, - @IStorageService private readonly _storageService: IStorageService, + @IStorageService private readonly _storageService: IStorageService ) { this._disposables = []; this._skipTerminalCommands = []; - this._onLineDataListeners = []; - this._onDataListeners = []; this._isExiting = false; this._hadFocusOnExit = false; this._isVisible = false; @@ -132,8 +141,10 @@ export class TerminalInstance implements ITerminalInstance { this._logService.trace(`terminalInstance#ctor (id: ${this.id})`, this._shellLaunchConfig); this._initDimensions(); - if (doCreateProcess) { + if (!this.shellLaunchConfig.isRendererOnly) { this._createProcess(); + } else { + this.setTitle(this._shellLaunchConfig.name, false); } this._xtermReadyPromise = this._createXterm(); @@ -144,14 +155,14 @@ export class TerminalInstance implements ITerminalInstance { } }); - this._configurationService.onDidChangeConfiguration(e => { + this.addDisposable(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('terminal.integrated')) { this.updateConfig(); } if (e.affectsConfiguration('editor.accessibilitySupport')) { this.updateAccessibilitySupport(); } - }); + })); } public addDisposable(disposable: lifecycle.IDisposable): void { @@ -194,7 +205,13 @@ export class TerminalInstance implements ITerminalInstance { // order to be precise. font.charWidth/charHeight alone as insufficient // when window.devicePixelRatio changes. const scaledWidthAvailable = dimension.width * window.devicePixelRatio; - const scaledCharWidth = Math.floor(font.charWidth * window.devicePixelRatio) + font.letterSpacing; + + let scaledCharWidth: number; + if (this._configHelper.config.rendererType === 'dom') { + scaledCharWidth = font.charWidth * window.devicePixelRatio; + } else { + scaledCharWidth = Math.floor(font.charWidth * window.devicePixelRatio) + font.letterSpacing; + } this._cols = Math.max(Math.floor(scaledWidthAvailable / scaledCharWidth), 1); const scaledHeightAvailable = dimension.height * window.devicePixelRatio; @@ -221,7 +238,7 @@ export class TerminalInstance implements ITerminalInstance { // it gets removed and then added back to the DOM (resetting scrollTop to 0). // Upstream issue: https://github.com/sourcelair/xterm.js/issues/291 if (this._xterm) { - this._xterm.emit('scroll', this._xterm.buffer.ydisp); + this._xterm.emit('scroll', this._xterm._core.buffer.ydisp); } } @@ -244,7 +261,7 @@ export class TerminalInstance implements ITerminalInstance { /** * Create xterm.js instance and attach data listeners. */ - protected async _createXterm(): TPromise { + protected async _createXterm(): Promise { if (!Terminal) { Terminal = (await import('vscode-xterm')).Terminal; // Enable xterm.js addons @@ -262,6 +279,7 @@ export class TerminalInstance implements ITerminalInstance { this._xterm = new Terminal({ scrollback: config.scrollback, theme: this._getXtermTheme(), + drawBoldTextInBrightColors: config.drawBoldTextInBrightColors, fontFamily: font.fontFamily, fontWeight: config.fontWeight, fontWeightBold: config.fontWeightBold, @@ -271,9 +289,12 @@ export class TerminalInstance implements ITerminalInstance { bellStyle: config.enableBell ? 'sound' : 'none', screenReaderMode: accessibilitySupport === 'on', macOptionIsMeta: config.macOptionIsMeta, + macOptionClickForcesSelection: config.macOptionClickForcesSelection, rightClickSelectsWord: config.rightClickBehavior === 'selectWord', // TODO: Guess whether to use canvas or dom better - rendererType: config.rendererType === 'auto' ? 'canvas' : config.rendererType + rendererType: config.rendererType === 'auto' ? 'canvas' : config.rendererType, + // TODO: Remove this once the setting is removed upstream + experimentalCharAtlas: 'dynamic' }); if (this._shellLaunchConfig.initialText) { this._xterm.writeln(this._shellLaunchConfig.initialText); @@ -284,8 +305,18 @@ export class TerminalInstance implements ITerminalInstance { this._processManager.onProcessData(data => this._onProcessData(data)); 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._processManager.initialCwd); + this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform); + this.processReady.then(() => { + this._linkHandler.initialCwd = this._processManager.initialCwd; + }); } + this._xterm.on('focus', () => this._onFocus.fire(this)); + + // Register listener to trigger the onInput ext API if the terminal is a renderer only + if (this._shellLaunchConfig.isRendererOnly) { + this._xterm.on('data', (data) => this._sendRendererInput(data)); + } + this._commandTracker = new TerminalCommandTracker(this._xterm); this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } @@ -439,8 +470,8 @@ export class TerminalInstance implements ITerminalInstance { } private _measureRenderTime(): void { - let frameTimes: number[] = []; - const textRenderLayer = (this._xterm).renderer._renderLayers[0]; + const frameTimes: number[] = []; + const textRenderLayer = this._xterm._core.renderer._renderLayers[0]; const originalOnGridChanged = textRenderLayer.onGridChanged; const evaluateCanvasRenderer = () => { @@ -454,7 +485,7 @@ export class TerminalInstance implements ITerminalInstance { label: nls.localize('yes', "Yes"), run: () => { this._configurationService.updateValue('terminal.integrated.rendererType', 'dom', ConfigurationTarget.USER).then(() => { - this._notificationService.info(nls.localize('terminal.rendererInAllNewTerminals', "All newly created terminals will use the non-GPU renderer.")); + this._notificationService.info(nls.localize('terminal.rendererInAllNewTerminals', "The terminal is now using the fallback renderer.")); }); } } as IPromptChoice, @@ -535,30 +566,33 @@ export class TerminalInstance implements ITerminalInstance { this._terminalFocusContextKey.set(terminalFocused); } - public dispose(): void { + public dispose(isShuttingDown?: boolean): void { this._logService.trace(`terminalInstance#dispose (id: ${this.id})`); - if (this._windowsShellHelper) { - this._windowsShellHelper.dispose(); - } - if (this._linkHandler) { - this._linkHandler.dispose(); - } + this._windowsShellHelper = lifecycle.dispose(this._windowsShellHelper); + this._linkHandler = lifecycle.dispose(this._linkHandler); + this._commandTracker = lifecycle.dispose(this._commandTracker); + this._widgetManager = lifecycle.dispose(this._widgetManager); + if (this._xterm && this._xterm.element) { this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus'); } if (this._wrapperElement) { + if ((this._wrapperElement).xterm) { + (this._wrapperElement).xterm = null; + } this._container.removeChild(this._wrapperElement); this._wrapperElement = null; + this._xtermElement = null; } if (this._xterm) { - const buffer = (this._xterm.buffer); + const buffer = (this._xterm._core.buffer); this._sendLineData(buffer, buffer.ybase + buffer.y); this._xterm.dispose(); this._xterm = null; } if (this._processManager) { - this._processManager.dispose(); + this._processManager.dispose(isShuttingDown); } if (!this._isDisposed) { this._isDisposed = true; @@ -577,22 +611,48 @@ export class TerminalInstance implements ITerminalInstance { } } + public focusWhenReady(force?: boolean): Promise { + return this._xtermReadyPromise.then(() => this.focus(force)); + } + public paste(): void { this.focus(); document.execCommand('paste'); } - public sendText(text: string, addNewLine: boolean): void { - this._processManager.ptyProcessReady.then(() => { - // Normalize line endings to 'enter' press. - text = text.replace(TerminalInstance.EOL_REGEX, '\r'); - if (addNewLine && text.substr(text.length - 1) !== '\r') { - text += '\r'; + public write(text: string): void { + this._xtermReadyPromise.then(() => { + if (!this._xterm) { + return; + } + this._xterm.write(text); + if (this._shellLaunchConfig.isRendererOnly) { + // Fire onData API in the extension host + this._onData.fire(text); } - this._processManager.write(text); }); } + public sendText(text: string, addNewLine: boolean): void { + // Normalize line endings to 'enter' press. + text = text.replace(TerminalInstance.EOL_REGEX, '\r'); + if (addNewLine && text.substr(text.length - 1) !== '\r') { + text += '\r'; + } + + if (this._shellLaunchConfig.isRendererOnly) { + // If the terminal is a renderer only, fire the onInput ext API + this._sendRendererInput(text); + } else { + // If the terminal has a process, send it to the process + if (this._processManager) { + this._processManager.ptyProcessReady.then(() => { + this._processManager.write(text); + }); + } + } + } + public setVisible(visible: boolean): void { this._isVisible = visible; if (this._wrapperElement) { @@ -603,7 +663,7 @@ export class TerminalInstance implements ITerminalInstance { // necessary if the number of rows in the terminal has decreased while it was in the // background since scrollTop changes take no effect but the terminal's position does // change since the number of visible rows decreases. - this._xterm.emit('scroll', this._xterm.buffer.ydisp); + this._xterm.emit('scroll', this._xterm._core.buffer.ydisp); if (this._container && this._container.parentElement) { // Force a layout when the instance becomes invisible. This is particularly important // for ensuring that terminals that are created in the background by an extension will @@ -613,6 +673,10 @@ export class TerminalInstance implements ITerminalInstance { const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); this.layout(new dom.Dimension(width, height)); + // HACK: Trigger another async layout to ensure xterm's CharMeasure is ready to use, + // this hack can be removed when https://github.com/xtermjs/xterm.js/issues/702 is + // supported. + setTimeout(() => this.layout(new dom.Dimension(width, height)), 0); } } } @@ -656,7 +720,7 @@ export class TerminalInstance implements ITerminalInstance { this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); - this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows); + this._processManager.onProcessData(data => this._onData.fire(data)); if (this._shellLaunchConfig.name) { this.setTitle(this._shellLaunchConfig.name, false); @@ -675,6 +739,12 @@ export class TerminalInstance implements ITerminalInstance { }); }); } + + // Create the process asynchronously to allow the terminal's container + // to be created so dimensions are accurate + setTimeout(() => { + this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows); + }, 0); } private _onProcessData(data: string): void { @@ -684,13 +754,6 @@ export class TerminalInstance implements ITerminalInstance { if (this._xterm) { this._xterm.write(data); } - this._onDataListeners.forEach(listener => { - try { - listener(data); - } catch (err) { - console.error(`onData listener threw`, err); - } - }); } private _onProcessExit(exitCode: number): void { @@ -756,6 +819,8 @@ export class TerminalInstance implements ITerminalInstance { } } } + + this._onExit.fire(exitCode); } private _attachPressAnyKeyToCloseListener() { @@ -796,35 +861,17 @@ export class TerminalInstance implements ITerminalInstance { this._shellLaunchConfig = shell; } - public onData(listener: (data: string) => void): lifecycle.IDisposable { - this._onDataListeners.push(listener); - return { - dispose: () => { - const i = this._onDataListeners.indexOf(listener); - if (i >= 0) { - this._onDataListeners.splice(i, 1); - } - } - }; - } + private _sendRendererInput(input: string): void { + if (this._processManager) { + throw new Error('onRendererInput attempted to be used on a regular terminal'); + } - public onLineData(listener: (lineData: string) => void): lifecycle.IDisposable { - this._onLineDataListeners.push(listener); - return { - dispose: () => { - const i = this._onLineDataListeners.indexOf(listener); - if (i >= 0) { - this._onLineDataListeners.splice(i, 1); - } - } - }; + // For terminal renderers onData fires on keystrokes and when sendText is called. + this._onRendererInput.fire(input); } private _onLineFeed(): void { - if (this._onLineDataListeners.length === 0) { - return; - } - const buffer = (this._xterm.buffer); + const buffer = (this._xterm._core.buffer); const newLine = buffer.lines.get(buffer.ybase + buffer.y); if (!newLine.isWrapped) { this._sendLineData(buffer, buffer.ybase + buffer.y - 1); @@ -836,27 +883,20 @@ export class TerminalInstance implements ITerminalInstance { while (lineIndex >= 0 && buffer.lines.get(lineIndex--).isWrapped) { lineData = buffer.translateBufferLineToString(lineIndex, false) + lineData; } - this._onLineDataListeners.forEach(listener => { - try { - listener(lineData); - } catch (err) { - console.error(`onLineData listener threw`, err); - } - }); - } - - public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable { - return this._processManager.onProcessExit(listener); + this._onLineData.fire(lineData); } public updateConfig(): void { - this._setCursorBlink(this._configHelper.config.cursorBlinking); - this._setCursorStyle(this._configHelper.config.cursorStyle); - this._setCommandsToSkipShell(this._configHelper.config.commandsToSkipShell); - this._setScrollback(this._configHelper.config.scrollback); - this._setEnableBell(this._configHelper.config.enableBell); - this._setMacOptionIsMeta(this._configHelper.config.macOptionIsMeta); - this._setRightClickSelectsWord(this._configHelper.config.rightClickBehavior === 'selectWord'); + const config = this._configHelper.config; + this._setCursorBlink(config.cursorBlinking); + this._setCursorStyle(config.cursorStyle); + this._setCommandsToSkipShell(config.commandsToSkipShell); + this._setEnableBell(config.enableBell); + this._safeSetOption('scrollback', config.scrollback); + this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); + this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); + this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); + this._safeSetOption('rendererType', config.rendererType === 'auto' ? 'canvas' : config.rendererType); } public updateAccessibilitySupport(): void { @@ -883,24 +923,6 @@ export class TerminalInstance implements ITerminalInstance { this._skipTerminalCommands = commands; } - private _setScrollback(lineCount: number): void { - if (this._xterm && this._xterm.getOption('scrollback') !== lineCount) { - this._xterm.setOption('scrollback', lineCount); - } - } - - private _setMacOptionIsMeta(value: boolean): void { - if (this._xterm && this._xterm.getOption('macOptionIsMeta') !== value) { - this._xterm.setOption('macOptionIsMeta', value); - } - } - - private _setRightClickSelectsWord(value: boolean): void { - if (this._xterm && this._xterm.getOption('rightClickSelectsWord') !== value) { - this._xterm.setOption('rightClickSelectsWord', value); - } - } - private _setEnableBell(isEnabled: boolean): void { if (this._xterm) { if (this._xterm.getOption('bellStyle') === 'sound') { @@ -915,6 +937,16 @@ export class TerminalInstance implements ITerminalInstance { } } + private _safeSetOption(key: string, value: any): void { + if (!this._xterm) { + return; + } + + if (this._xterm.getOption(key) !== value) { + this._xterm.setOption(key, value); + } + } + public layout(dimension: dom.Dimension): void { if (this.disableLayout) { return; @@ -925,47 +957,58 @@ export class TerminalInstance implements ITerminalInstance { return; } + if (this._xterm) { + this._xterm.element.style.width = terminalWidth + 'px'; + } + + this._resize(); + } + + private _resize(): void { + let cols = this._cols; + let rows = this._rows; + if (this._dimensionsOverride && this._dimensionsOverride.cols && this._dimensionsOverride.rows) { + cols = Math.min(Math.max(this._dimensionsOverride.cols, 2), this._cols); + rows = Math.min(Math.max(this._dimensionsOverride.rows, 2), this._rows); + } + if (this._xterm) { const font = this._configHelper.getFont(this._xterm); // Only apply these settings when the terminal is visible so that // the characters are measured correctly. if (this._isVisible) { - if (this._xterm.getOption('letterSpacing') !== font.letterSpacing) { - this._xterm.setOption('letterSpacing', font.letterSpacing); - } - if (this._xterm.getOption('lineHeight') !== font.lineHeight) { - this._xterm.setOption('lineHeight', font.lineHeight); - } - if (this._xterm.getOption('fontSize') !== font.fontSize) { - this._xterm.setOption('fontSize', font.fontSize); - } - if (this._xterm.getOption('fontFamily') !== font.fontFamily) { - this._xterm.setOption('fontFamily', font.fontFamily); - } - if (this._xterm.getOption('fontWeight') !== this._configHelper.config.fontWeight) { - this._xterm.setOption('fontWeight', this._configHelper.config.fontWeight); - } - if (this._xterm.getOption('fontWeightBold') !== this._configHelper.config.fontWeightBold) { - this._xterm.setOption('fontWeightBold', this._configHelper.config.fontWeightBold); - } + const config = this._configHelper.config; + this._safeSetOption('letterSpacing', font.letterSpacing); + this._safeSetOption('lineHeight', font.lineHeight); + this._safeSetOption('fontSize', font.fontSize); + this._safeSetOption('fontFamily', font.fontFamily); + this._safeSetOption('fontWeight', config.fontWeight); + this._safeSetOption('fontWeightBold', config.fontWeightBold); + this._safeSetOption('drawBoldTextInBrightColors', config.drawBoldTextInBrightColors); } - this._xterm.resize(this._cols, this._rows); - this._xterm.element.style.width = terminalWidth + 'px'; + if (cols !== this._xterm.cols || rows !== this._xterm.rows) { + this._onDimensionsChanged.fire(); + } + + this._xterm.resize(cols, rows); if (this._isVisible) { - // Force the renderer to unpause by simulating an IntersectionObserver event. This - // is to fix an issue where dragging the window to the top of the screen to maximize - // on Winodws/Linux would fire an event saying that the terminal was not visible. - // This should only force a refresh if one is needed. + // HACK: Force the renderer to unpause by simulating an IntersectionObserver event. + // This is to fix an issue where dragging the window to the top of the screen to + // maximize on Windows/Linux would fire an event saying that the terminal was not + // visible. if (this._xterm.getOption('rendererType') === 'canvas') { - (this._xterm).renderer.onIntersectionChange({ intersectionRatio: 1 }); + this._xterm._core.renderer.onIntersectionChange({ intersectionRatio: 1 }); + // HACK: Force a refresh of the screen to ensure links are refresh corrected. + // This can probably be removed when the above hack is fixed in Chromium. + this._xterm.refresh(0, this._xterm.rows - 1); } } } if (this._processManager) { - this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows)); + this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(cols, rows)); } } @@ -994,6 +1037,11 @@ export class TerminalInstance implements ITerminalInstance { } } + public setDimensions(dimensions: ITerminalDimensions): void { + this._dimensionsOverride = dimensions; + this._resize(); + } + private _getXtermTheme(theme?: ITheme): any { if (!theme) { theme = this._themeService.getTheme(); @@ -1033,6 +1081,11 @@ export class TerminalInstance implements ITerminalInstance { private _updateTheme(theme?: ITheme): void { this._xterm.setOption('theme', this._getXtermTheme(theme)); } + + public toggleEscapeSequenceLogging(): void { + this._xterm._core.debug = !this._xterm._core.debug; + this._xterm.setOption('debug', this._xterm._core.debug); + } } registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { @@ -1049,6 +1102,7 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const scrollbarSliderBackgroundColor = theme.getColor(scrollbarSliderBackground); if (scrollbarSliderBackgroundColor) { collector.addRule(` + .monaco-workbench .panel.integrated-terminal .find-focused .xterm .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm.focus .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm:focus .xterm-viewport, .monaco-workbench .panel.integrated-terminal .xterm:hover .xterm-viewport { background-color: ${scrollbarSliderBackgroundColor} !important; }` diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts index d19528a9e31..9186939137f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as path from 'path'; import * as platform from 'vs/base/common/platform'; import * as pfs from 'vs/base/node/pfs'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TerminalWidgetManager } from 'vs/workbench/parts/terminal/browser/terminalWidgetManager'; @@ -61,13 +61,13 @@ export class TerminalLinkHandler { private _hoverDisposables: IDisposable[] = []; private _mouseMoveDisposable: IDisposable; private _widgetManager: TerminalWidgetManager; + private _initialCwd: string; private _localLinkPattern: RegExp; constructor( private _xterm: any, private _platform: platform.Platform, - private _initialCwd: string, @IOpenerService private readonly _openerService: IOpenerService, @IEditorService private readonly _editorService: IEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -84,6 +84,10 @@ export class TerminalLinkHandler { this._widgetManager = widgetManager; } + public set initialCwd(initialCwd: string) { + this._initialCwd = initialCwd; + } + public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number { return this._xterm.registerLinkMatcher(regex, this._wrapLinkHandler(handler), { matchIndex, @@ -121,6 +125,7 @@ export class TerminalLinkHandler { } public dispose(): void { + this._xterm = null; this._hoverDisposables = dispose(this._hoverDisposables); this._mouseMoveDisposable = dispose(this._mouseMoveDisposable); } @@ -146,10 +151,6 @@ export class TerminalLinkHandler { private _handleLocalLink(link: string): TPromise { return this._resolvePath(link).then(resolvedLink => { - if (!resolvedLink) { - return void 0; - } - const normalizedPath = path.normalize(path.resolve(resolvedLink)); const normalizedUrl = this.extractLinkUrl(normalizedPath); const resource = Uri.file(normalizedUrl); @@ -172,7 +173,7 @@ export class TerminalLinkHandler { } private _handleHypertextLink(url: string): void { - let uri = Uri.parse(url); + const uri = Uri.parse(url); this._openerService.open(uri); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 09e090d2b5c..11e11fb473d 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -21,14 +21,15 @@ import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, Cop import { Panel } from 'vs/workbench/browser/panel'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { PANEL_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; +const FIND_FOCUS_CLASS = 'find-focused'; + export class TerminalPanel extends Panel { private _actions: IAction[]; @@ -45,7 +46,6 @@ export class TerminalPanel extends Panel { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalService private readonly _terminalService: ITerminalService, - @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IThemeService protected themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService @@ -63,6 +63,8 @@ export class TerminalPanel extends Panel { dom.addClass(this._terminalContainer, 'terminal-outer-container'); this._findWidget = this._instantiationService.createInstance(TerminalFindWidget); + this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer.classList.add(FIND_FOCUS_CLASS)); + this._findWidget.focusTracker.onDidBlur(() => this._terminalContainer.classList.remove(FIND_FOCUS_CLASS)); this._parentDomElement.appendChild(this._fontStyleElement); this._parentDomElement.appendChild(this._terminalContainer); @@ -79,7 +81,7 @@ export class TerminalPanel extends Panel { } if (e.affectsConfiguration('terminal.integrated.fontFamily') || e.affectsConfiguration('editor.fontFamily')) { - let configHelper = this._terminalService.configHelper; + const configHelper = this._terminalService.configHelper; if (configHelper instanceof TerminalConfigHelper) { if (!configHelper.configFontIsMonospace()) { const choices: IPromptChoice[] = [{ @@ -113,27 +115,14 @@ export class TerminalPanel extends Panel { this._updateTheme(); } else { return super.setVisible(visible).then(() => { - // Ensure the "Running" lifecycle face has been reached before creating the - // first terminal. - this._lifecycleService.when(LifecyclePhase.Running).then(() => { - // Allow time for the panel to display if it is being shown - // for the first time. If there is not wait here the initial - // dimensions of the pty could be wrong. - setTimeout(() => { - // Check if instances were already restored as part of workbench restore - if (this._terminalService.terminalInstances.length > 0) { - this._updateFont(); - this._updateTheme(); - return; - } - - const instance = this._terminalService.createTerminal(); - if (instance) { - this._updateFont(); - this._updateTheme(); - } - }, 0); - }); + // Check if instances were already restored as part of workbench restore + if (this._terminalService.terminalInstances.length === 0) { + this._terminalService.createTerminal(); + } + if (this._terminalService.terminalInstances.length > 0) { + this._updateFont(); + this._updateTheme(); + } return TPromise.as(void 0); }); } @@ -145,7 +134,7 @@ export class TerminalPanel extends Panel { if (!this._actions) { this._actions = [ this._instantiationService.createInstance(SwitchTerminalAction, SwitchTerminalAction.ID, SwitchTerminalAction.LABEL), - this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.PANEL_LABEL), + this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL), this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL), this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL) ]; @@ -158,16 +147,16 @@ export class TerminalPanel extends Panel { private _getContextMenuActions(): IAction[] { if (!this._contextMenuActions) { - this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, nls.localize('copy', "Copy")); + this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.SHORT_LABEL); this._contextMenuActions = [ - this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.PANEL_LABEL), - this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, nls.localize('split', "Split")), + this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL), + this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.SHORT_LABEL), new Separator(), this._copyContextMenuAction, - this._instantiationService.createInstance(TerminalPasteAction, TerminalPasteAction.ID, nls.localize('paste', "Paste")), - this._instantiationService.createInstance(SelectAllTerminalAction, SelectAllTerminalAction.ID, nls.localize('selectAll', "Select All")), + 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, nls.localize('clear', "Clear")) + this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, ClearTerminalAction.LABEL) ]; this._contextMenuActions.forEach(a => { this._register(a); @@ -189,7 +178,7 @@ export class TerminalPanel extends Panel { public focus(): void { const activeInstance = this._terminalService.getActiveInstance(); if (activeInstance) { - activeInstance.focus(true); + activeInstance.focusWhenReady(true); } } @@ -206,14 +195,6 @@ export class TerminalPanel extends Panel { this._findWidget.hide(); } - public showNextFindTermFindWidget(): void { - this._findWidget.showNextFindTerm(); - } - - public showPreviousFindTermFindWidget(): void { - this._findWidget.showPreviousFindTerm(); - } - private _attachEventListeners(): void { this._register(dom.addDisposableListener(this._parentDomElement, 'mousedown', (event: MouseEvent) => { if (this._terminalService.terminalInstances.length === 0) { @@ -226,7 +207,7 @@ export class TerminalPanel extends Panel { this._terminalService.getActiveInstance().focus(); } else if (event.which === 3) { if (this._terminalService.configHelper.config.rightClickBehavior === 'copyPaste') { - let terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalService.getActiveInstance(); if (terminal.hasSelection()) { terminal.copySelection(); terminal.clearSelection(); @@ -253,7 +234,7 @@ export class TerminalPanel extends Panel { } if (event.which === 1) { - let terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalService.getActiveInstance(); if (terminal.hasSelection()) { terminal.copySelection(); } @@ -263,7 +244,7 @@ export class TerminalPanel extends Panel { this._register(dom.addDisposableListener(this._parentDomElement, 'contextmenu', (event: MouseEvent) => { if (!this._cancelContextMenu) { const standardEvent = new StandardMouseEvent(event); - let anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy }; + const anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy }; this._contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => TPromise.as(this._getContextMenuActions()), @@ -292,7 +273,7 @@ export class TerminalPanel extends Panel { // Check if files were dragged from the tree explorer let path: string; - let resources = e.dataTransfer.getData(DataTransfers.RESOURCES); + const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); if (resources) { path = URI.parse(JSON.parse(resources)[0]).path; } else if (e.dataTransfer.files.length > 0) { @@ -338,15 +319,15 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } // Borrow the editor's hover background for now - let hoverBackground = theme.getColor(editorHoverBackground); + const hoverBackground = theme.getColor(editorHoverBackground); if (hoverBackground) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`); } - let hoverBorder = theme.getColor(editorHoverBorder); + const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`); } - let hoverForeground = theme.getColor(editorForeground); + const hoverForeground = theme.getColor(editorForeground); if (hoverForeground) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index 1670b248a0b..818b359ea99 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; -import Uri from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { ITerminalChildProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/node/terminalProcessExtHostProxy'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { Schemas } from 'vs/base/common/network'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -50,13 +50,13 @@ export class TerminalProcessManager implements ITerminalProcessManager { public get onProcessExit(): Event { return this._onProcessExit.event; } constructor( - private _terminalId: number, - private _configHelper: ITerminalConfigHelper, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + private readonly _terminalId: number, + private readonly _configHelper: ITerminalConfigHelper, @IHistoryService private readonly _historyService: IHistoryService, - @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private _logService: ILogService + @ILogService private readonly _logService: ILogService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService ) { this.ptyProcessReady = new TPromise(c => { this.onProcessReady(() => { @@ -66,15 +66,13 @@ export class TerminalProcessManager implements ITerminalProcessManager { }); } - public dispose(): void { + public dispose(immediate?: boolean): void { if (this._process) { - if (this._process.connected) { - // If the process was still connected this dispose came from - // within VS Code, not the process, so mark the process as - // killed by the user. - this.processState = ProcessState.KILLED_BY_USER; - this._process.send({ event: 'shutdown' }); - } + // If the process was still connected this dispose came from + // within VS Code, not the process, so mark the process as + // killed by the user. + this.processState = ProcessState.KILLED_BY_USER; + this._process.shutdown(immediate); this._process = null; } this._disposables.forEach(d => d.dispose()); @@ -94,12 +92,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (extensionHostOwned) { this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, cols, rows); } else { - const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; if (!shellLaunchConfig.executable) { this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); } - const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); // Resolve env vars from config and shell @@ -109,23 +106,41 @@ export class TerminalProcessManager implements ITerminalProcessManager { const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); shellLaunchConfig.env = envFromShell; - // Merge process env with the env from config - const parentEnv = { ...process.env }; - terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + // Merge process env with the env from config and from shellLaunchConfig + const env = { ...process.env }; + terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); - // Continue env initialization, merging in the env from the launch - // config and adding keys that are needed to create the process - const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows); - const cwd = Uri.parse(require.toUrl('../node')).fsPath; - const options = { env, cwd }; - this._logService.debug(`Terminal process launching`, options); + // Sanitize the environment, removing any undesirable VS Code and Electron environment + // variables + terminalEnvironment.sanitizeEnvironment(env); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + // Adding other env keys necessary to create the process + const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + + this._logService.debug(`Terminal process launching`, shellLaunchConfig, this.initialCwd, cols, rows, env); + this._process = new TerminalProcess(shellLaunchConfig, this.initialCwd, cols, rows, env); } this.processState = ProcessState.LAUNCHING; - this._process.on('message', message => this._onMessage(message)); - this._process.on('exit', exitCode => this._onExit(exitCode)); + this._process.onProcessData(data => { + this._onProcessData.fire(data); + }); + + this._process.onProcessIdReady(pid => { + this.shellProcessId = pid; + this._onProcessReady.fire(); + + // Send any queued data that's waiting + if (this._preLaunchInputQueue.length > 0) { + this._process.input(this._preLaunchInputQueue.join('')); + this._preLaunchInputQueue.length = 0; + } + }); + + this._process.onProcessTitleChanged(title => this._onProcessTitle.fire(title)); + this._process.onProcessExit(exitCode => this._onExit(exitCode)); setTimeout(() => { if (this.processState === ProcessState.LAUNCHING) { @@ -135,57 +150,33 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public setDimensions(cols: number, rows: number): void { - if (this._process && this._process.connected) { - // The child process could aready be terminated - try { - this._process.send({ event: 'resize', cols, rows }); - } catch (error) { - // We tried to write to a closed pipe / channel. - if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { - throw (error); - } + if (!this._process) { + return; + } + + // The child process could already be terminated + try { + this._process.resize(cols, rows); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); } } } public write(data: string): void { if (this.shellProcessId) { - // Send data if the pty is ready - this._process.send({ - event: 'input', - data - }); + if (this._process) { + // Send data if the pty is ready + this._process.input(data); + } } else { // If the pty is not ready, queue the data received to send later this._preLaunchInputQueue.push(data); } } - private _onMessage(message: IMessageFromTerminalProcess): void { - this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); - switch (message.type) { - case 'data': - this._onProcessData.fire(message.content); - break; - case 'pid': - this.shellProcessId = message.content; - this._onProcessReady.fire(); - - // Send any queued data that's waiting - if (this._preLaunchInputQueue.length > 0) { - this._process.send({ - event: 'input', - data: this._preLaunchInputQueue.join('') - }); - this._preLaunchInputQueue.length = 0; - } - break; - case 'title': - this._onProcessTitle.fire(message.content); - break; - } - } - private _onExit(exitCode: number): void { this._process = null; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index 370cb709b9b..fb61123763f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -13,7 +13,6 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IQuickOpenService, IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID, ITerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/common/terminal'; import { TerminalService as AbstractTerminalService } from 'vs/workbench/parts/terminal/common/terminalService'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; @@ -29,6 +28,7 @@ import { ipcRenderer as ipc } from 'electron'; import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; export class TerminalService extends AbstractTerminalService implements ITerminalService { private _configHelper: TerminalConfigHelper; @@ -47,7 +47,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina @ILifecycleService lifecycleService: ILifecycleService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, @INotificationService private readonly _notificationService: INotificationService, @IDialogService private readonly _dialogService: IDialogService, @IExtensionService private readonly _extensionService: IExtensionService @@ -91,8 +91,12 @@ export class TerminalService extends AbstractTerminalService implements ITermina return instance; } + public createTerminalRenderer(name: string): ITerminalInstance { + return this.createTerminal({ name, isRendererOnly: true }); + } + public createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance { - const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig, true); + const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig); this._onInstanceCreated.fire(instance); return instance; } @@ -109,7 +113,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina public focusFindWidget(): TPromise { return this.showPanel(false).then(() => { - let panel = this._panelService.getActivePanel() as TerminalPanel; + const panel = this._panelService.getActivePanel() as TerminalPanel; panel.focusFindWidget(); this._findWidgetVisible.set(true); }); @@ -124,20 +128,6 @@ export class TerminalService extends AbstractTerminalService implements ITermina } } - public showNextFindTermFindWidget(): void { - const panel = this._panelService.getActivePanel() as TerminalPanel; - if (panel && panel.getId() === TERMINAL_PANEL_ID) { - panel.showNextFindTermFindWidget(); - } - } - - public showPreviousFindTermFindWidget(): void { - const panel = this._panelService.getActivePanel() as TerminalPanel; - if (panel && panel.getId() === TERMINAL_PANEL_ID) { - panel.showPreviousFindTermFindWidget(); - } - } - private _suggestShellChange(wasNewTerminalAction?: boolean): void { // Only suggest on Windows since $SHELL works great for macOS/Linux if (!platform.isWindows) { @@ -194,10 +184,10 @@ export class TerminalService extends AbstractTerminalService implements ITermina public selectDefaultWindowsShell(): TPromise { return this._detectWindowsShells().then(shells => { - const options: IPickOptions = { + const options: IPickOptions = { placeHolder: nls.localize('terminal.integrated.chooseWindowsShell', "Select your preferred terminal shell, you can change this later in your settings") }; - return this._quickOpenService.pick(shells, options).then(value => { + return this._quickInputService.pick(shells, options).then(value => { if (!value) { return null; } @@ -207,7 +197,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina }); } - private _detectWindowsShells(): TPromise { + private _detectWindowsShells(): TPromise { // Determine the correct System32 path. We want to point to Sysnative // when the 32-bit version of VS Code is running on a 64-bit machine. // The reason for this is because PowerShell's important PSReadline @@ -218,7 +208,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release()); let useWSLexe = false; - if (osVersion.length === 4) { + if (osVersion && osVersion.length === 4) { const buildNumber = parseInt(osVersion[3]); if (buildNumber >= 16299) { useWSLexe = true; @@ -241,7 +231,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina Object.keys(expectedLocations).forEach(key => promises.push(this._validateShellPaths(key, expectedLocations[key]))); return TPromise.join(promises).then(results => { return results.filter(result => !!result).map(result => { - return { + return { label: result[0], description: result[1] }; diff --git a/src/vs/workbench/parts/terminal/node/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts index 8cb9bc9dddd..7eaadc133aa 100644 --- a/src/vs/workbench/parts/terminal/node/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -7,30 +7,27 @@ import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import * as processes from 'vs/base/node/processes'; import { readFile, fileExists } from 'vs/base/node/pfs'; - -export interface IMessageFromTerminalProcess { - type: 'pid' | 'data' | 'title'; - content: number | string; -} - -export interface IMessageToTerminalProcess { - event: 'resize' | 'input' | 'shutdown'; - data?: string; - cols?: number; - rows?: number; -} +import { Event } from 'vs/base/common/event'; /** * An interface representing a raw terminal child process, this is a subset of the * child_process.ChildProcess node.js interface. */ export interface ITerminalChildProcess { - readonly connected: boolean; + onProcessData: Event; + onProcessExit: Event; + onProcessIdReady: Event; + onProcessTitleChanged: Event; - send(message: IMessageToTerminalProcess): boolean; - - on(event: 'exit', listener: (code: number) => void): this; - on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this; + /** + * Shutdown the terminal process. + * + * @param immediate When true the process will be killed immediately, otherwise the process will + * be given some time to make sure no additional data comes through. + */ + shutdown(immediate: boolean): void; + input(data: string): void; + resize(cols: number, rows: number): void; } let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null; diff --git a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts index d11af07bd79..a6f1dfed010 100644 --- a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts +++ b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts @@ -5,6 +5,7 @@ import { Terminal, IMarker } from 'vscode-xterm'; import { ITerminalCommandTracker } from 'vs/workbench/parts/terminal/common/terminal'; +import { IDisposable } from 'vs/base/common/lifecycle'; /** * The minimize size of the prompt in which to assume the line is a command. @@ -16,12 +17,12 @@ enum Boundary { Bottom } -export enum ScrollPosition { +export const enum ScrollPosition { Top, Middle } -export class TerminalCommandTracker implements ITerminalCommandTracker { +export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposable { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; private _isDisposable: boolean = false; @@ -32,6 +33,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { this._xterm.on('key', key => this._onKey(key)); } + public dispose(): void { + this._xterm = null; + } + private _onKey(key: string): void { if (key === '\x0d') { this._onEnter(); @@ -44,7 +49,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { } private _onEnter(): void { - if (this._xterm.buffer.x >= MINIMUM_PROMPT_LENGTH) { + if (this._xterm._core.buffer.x >= MINIMUM_PROMPT_LENGTH) { this._xterm.addMarker(0); } } @@ -171,7 +176,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { private _getLine(marker: IMarker | Boundary): number { // Use the _second last_ row as the last row is likely the prompt if (marker === Boundary.Bottom) { - return this._xterm.buffer.ybase + this._xterm.rows - 1; + return this._xterm._core.buffer.ybase + this._xterm.rows - 1; } if (marker === Boundary.Top) { @@ -194,7 +199,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._currentMarker === Boundary.Bottom) { this._currentMarker = this._xterm.addMarker(this._getOffset() - 1); } else { - let offset = this._getOffset(); + const offset = this._getOffset(); if (this._isDisposable) { this._currentMarker.dispose(); } @@ -217,7 +222,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._currentMarker === Boundary.Top) { this._currentMarker = this._xterm.addMarker(this._getOffset() + 1); } else { - let offset = this._getOffset(); + const offset = this._getOffset(); if (this._isDisposable) { this._currentMarker.dispose(); } @@ -231,10 +236,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._currentMarker === Boundary.Bottom) { return 0; } else if (this._currentMarker === Boundary.Top) { - return 0 - (this._xterm.buffer.ybase + this._xterm.buffer.y); + return 0 - (this._xterm._core.buffer.ybase + this._xterm._core.buffer.y); } else { let offset = this._getLine(this._currentMarker); - offset -= this._xterm.buffer.ybase + this._xterm.buffer.y; + offset -= this._xterm._core.buffer.ybase + this._xterm._core.buffer.y; return offset; } } diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index 2c60754603d..bf59acc1023 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -7,8 +7,7 @@ import * as os from 'os'; import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import pkg from 'vs/platform/node/package'; -import Uri from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { URI as Uri } from 'vs/base/common/uri'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -17,7 +16,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati * This module contains utility functions related to the environment, cwd and paths. */ -export function mergeEnvironments(parent: IStringDictionary, other: IStringDictionary) { +export function mergeEnvironments(parent: platform.IProcessEnvironment, other: platform.IProcessEnvironment): void { if (!other) { return; } @@ -25,9 +24,9 @@ export function mergeEnvironments(parent: IStringDictionary, other: IStr // On Windows apply the new values ignoring case, while still retaining // the case of the original key. if (platform.isWindows) { - for (let configKey in other) { + for (const configKey in other) { let actualKey = configKey; - for (let envKey in parent) { + for (const envKey in parent) { if (configKey.toLowerCase() === envKey.toLowerCase()) { actualKey = envKey; break; @@ -44,7 +43,7 @@ export function mergeEnvironments(parent: IStringDictionary, other: IStr } } -function _mergeEnvironmentValue(env: IStringDictionary, key: string, value: string | null) { +function _mergeEnvironmentValue(env: platform.IProcessEnvironment, key: string, value: string | null): void { if (typeof value === 'string') { env[key] = value; } else { @@ -52,34 +51,44 @@ function _mergeEnvironmentValue(env: IStringDictionary, key: string, val } } -export function createTerminalEnv(parentEnv: IStringDictionary, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary { - const env = { ...parentEnv }; - if (shell.env) { - mergeEnvironments(env, shell.env); - } - - env['PTYPID'] = process.pid.toString(); - env['PTYSHELL'] = shell.executable; - env['TERM_PROGRAM'] = 'vscode'; - env['TERM_PROGRAM_VERSION'] = pkg.version; - if (shell.args) { - if (typeof shell.args === 'string') { - env[`PTYSHELLCMDLINE`] = shell.args; - } else { - shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg); +export function sanitizeEnvironment(env: platform.IProcessEnvironment): void { + // Remove keys based on strings + const keysToRemove = [ + 'ELECTRON_ENABLE_STACK_DUMPING', + 'ELECTRON_ENABLE_LOGGING', + 'ELECTRON_NO_ASAR', + 'ELECTRON_NO_ATTACH_CONSOLE', + 'ELECTRON_RUN_AS_NODE', + 'GOOGLE_API_KEY', + 'VSCODE_CLI', + 'VSCODE_DEV', + 'VSCODE_IPC_HOOK', + 'VSCODE_LOGS', + 'VSCODE_NLS_CONFIG', + 'VSCODE_PORTABLE', + 'VSCODE_PID', + ]; + keysToRemove.forEach((key) => { + if (env[key]) { + delete env[key]; } - } - env['PTYCWD'] = cwd; - env['LANG'] = _getLangEnvVariable(locale); - if (cols && rows) { - env['PTYCOLS'] = cols.toString(); - env['PTYROWS'] = rows.toString(); - } - env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; - return env; + }); + + // Remove keys based on regexp + Object.keys(env).forEach(key => { + if (key.search(/^VSCODE_NODE_CACHED_DATA_DIR_\d+$/) === 0) { + delete env[key]; + } + }); } -export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { +export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, locale: string | undefined): void { + env['TERM_PROGRAM'] = 'vscode'; + env['TERM_PROGRAM_VERSION'] = pkg.version; + env['LANG'] = _getLangEnvVariable(locale); +} + +export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: platform.IProcessEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder): platform.IProcessEnvironment { Object.keys(env).forEach((key) => { if (typeof env[key] === 'string') { env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]); diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index e3a73831538..03beafef97b 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -5,161 +5,137 @@ import * as os from 'os'; import * as path from 'path'; +import * as platform from 'vs/base/common/platform'; import * as pty from 'node-pty'; +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'; -// The pty process needs to be run in its own child process to get around maxing out CPU on Mac, -// see https://github.com/electron/electron/issues/38 -var shellName: string; -if (os.platform() === 'win32') { - shellName = path.basename(process.env.PTYSHELL); -} else { - // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a - // color prompt as defined in the default ~/.bashrc file. - shellName = 'xterm-256color'; -} -var shell = process.env.PTYSHELL; -var args = getArgs(); -var cwd = process.env.PTYCWD; -var cols = process.env.PTYCOLS; -var rows = process.env.PTYROWS; -var currentTitle = ''; +export class TerminalProcess implements ITerminalChildProcess, IDisposable { + private _exitCode: number; + private _closeTimeout: number; + private _ptyProcess: pty.IPty; + private _currentTitle: string = ''; -setupPlanB(Number(process.env.PTYPID)); -cleanEnv(); + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } -interface IOptions { - name: string; - cwd: string; - cols?: number; - rows?: number; -} + constructor( + shellLaunchConfig: IShellLaunchConfig, + cwd: string, + cols: number, + rows: number, + env: platform.IProcessEnvironment + ) { + let shellName: string; + if (os.platform() === 'win32') { + shellName = path.basename(shellLaunchConfig.executable); + } else { + // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a + // color prompt as defined in the default ~/.bashrc file. + shellName = 'xterm-256color'; + } -var options: IOptions = { - name: shellName, - cwd -}; -if (cols && rows) { - options.cols = parseInt(cols, 10); - options.rows = parseInt(rows, 10); -} + const options: pty.IPtyForkOptions = { + name: shellName, + cwd, + env, + cols, + rows + }; -var ptyProcess = pty.spawn(shell, args, options); + this._ptyProcess = pty.spawn(shellLaunchConfig.executable, shellLaunchConfig.args, options); + this._ptyProcess.on('data', (data) => { + this._onProcessData.fire(data); + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + this._queueProcessExit(); + } + }); + this._ptyProcess.on('exit', (code) => { + this._exitCode = code; + this._queueProcessExit(); + }); -var closeTimeout: number; -var exitCode: number; - -// Allow any trailing data events to be sent before the exit event is sent. -// See https://github.com/Tyriar/node-pty/issues/72 -function queueProcessExit() { - if (closeTimeout) { - clearTimeout(closeTimeout); + // TODO: We should no longer need to delay this since pty.spawn is sync + setTimeout(() => { + this._sendProcessId(); + }, 500); + this._setupTitlePolling(); } - closeTimeout = setTimeout(function () { - ptyProcess.kill(); - process.exit(exitCode); - }, 250); -} -ptyProcess.on('data', function (data) { - process.send({ - type: 'data', - content: data - }); - if (closeTimeout) { - clearTimeout(closeTimeout); - queueProcessExit(); + public dispose(): void { + this._onProcessData.dispose(); + this._onProcessExit.dispose(); + this._onProcessIdReady.dispose(); + this._onProcessTitleChanged.dispose(); } -}); -ptyProcess.on('exit', function (code) { - exitCode = code; - queueProcessExit(); -}); + private _setupTitlePolling() { + // Send initial timeout async to give event listeners a chance to init + setTimeout(() => { + this._sendProcessTitle(); + }, 0); + // Setup polling + setInterval(() => { + if (this._currentTitle !== this._ptyProcess.process) { + this._sendProcessTitle(); + } + }, 200); + } -process.on('message', function (message) { - if (message.event === 'input') { - ptyProcess.write(message.data); - } else if (message.event === 'resize') { + // Allow any trailing data events to be sent before the exit event is sent. + // See https://github.com/Tyriar/node-pty/issues/72 + private _queueProcessExit() { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + this._closeTimeout = setTimeout(() => this._kill(), 250); + } + + private _kill(): void { + // Attempt to kill the pty, it may have already been killed at this + // point but we want to make sure + try { + this._ptyProcess.kill(); + } catch (ex) { + // Swallow, the pty has already been killed + } + this._onProcessExit.fire(this._exitCode); + this.dispose(); + } + + private _sendProcessId() { + this._onProcessIdReady.fire(this._ptyProcess.pid); + } + + private _sendProcessTitle(): void { + this._currentTitle = this._ptyProcess.process; + this._onProcessTitleChanged.fire(this._currentTitle); + } + + public shutdown(immediate: boolean): void { + if (immediate) { + this._kill(); + } else { + this._queueProcessExit(); + } + } + + public input(data: string): void { + this._ptyProcess.write(data); + } + + public resize(cols: number, rows: number): void { // Ensure that cols and rows are always >= 1, this prevents a native // exception in winpty. - ptyProcess.resize(Math.max(message.cols, 1), Math.max(message.rows, 1)); - } else if (message.event === 'shutdown') { - queueProcessExit(); - } -}); - -sendProcessId(); -setupTitlePolling(); - -function getArgs(): string | string[] { - if (process.env['PTYSHELLCMDLINE']) { - return process.env['PTYSHELLCMDLINE']; - } - var args = []; - var i = 0; - while (process.env['PTYSHELLARG' + i]) { - args.push(process.env['PTYSHELLARG' + i]); - i++; - } - return args; -} - -function cleanEnv() { - var keys = [ - 'AMD_ENTRYPOINT', - 'ELECTRON_NO_ASAR', - 'ELECTRON_RUN_AS_NODE', - 'GOOGLE_API_KEY', - 'PTYCWD', - 'PTYPID', - 'PTYSHELL', - 'PTYCOLS', - 'PTYROWS', - 'PTYSHELLCMDLINE', - 'VSCODE_LOGS' - ]; - keys.forEach(function (key) { - if (process.env[key]) { - delete process.env[key]; - } - }); - var i = 0; - while (process.env['PTYSHELLARG' + i]) { - delete process.env['PTYSHELLARG' + i]; - i++; + this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1)); } } - -function setupPlanB(parentPid: number) { - setInterval(function () { - try { - process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore. - } catch (e) { - process.exit(); - } - }, 5000); -} - -function sendProcessId() { - process.send({ - type: 'pid', - content: ptyProcess.pid - }); -} - -function setupTitlePolling() { - sendProcessTitle(); - setInterval(function () { - if (currentTitle !== ptyProcess.process) { - sendProcessTitle(); - } - }, 200); -} - -function sendProcessTitle() { - process.send({ - type: 'title', - content: ptyProcess.process - }); - currentTitle = ptyProcess.process; -} diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts index 9c842fbbfa8..08e3144893c 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -3,28 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITerminalChildProcess, IMessageToTerminalProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; -import { EventEmitter } from 'events'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { Event, Emitter } from 'vs/base/common/event'; import { ITerminalService, ITerminalProcessExtHostProxy, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; - -export class TerminalProcessExtHostProxy extends EventEmitter implements ITerminalChildProcess, ITerminalProcessExtHostProxy { - // For ext host processes connected checks happen on the ext host - public connected: boolean = true; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerminalProcessExtHostProxy { private _disposables: IDisposable[] = []; + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + + private readonly _onInput: Emitter = new Emitter(); + public get onInput(): Event { return this._onInput.event; } + private readonly _onResize: Emitter<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>(); + public get onResize(): Event<{ cols: number, rows: number }> { return this._onResize.event; } + private readonly _onShutdown: Emitter = new Emitter(); + public get onShutdown(): Event { return this._onShutdown.event; } + constructor( public terminalId: number, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, - @ITerminalService private _terminalService: ITerminalService + @ITerminalService private _terminalService: ITerminalService, + @IExtensionService private readonly _extensionService: IExtensionService ) { - super(); - - // TODO: Return TPromise indicating success? Teardown if failure? - this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); + this._extensionService.whenInstalledExtensionsRegistered().then(() => { + // TODO: MainThreadTerminalService is not ready at this point, fix this + setTimeout(() => { + this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); + }, 0); + }); } public dispose(): void { @@ -33,46 +50,31 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin } public emitData(data: string): void { - this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess); + this._onProcessData.fire(data); } public emitTitle(title: string): void { - this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess); + this._onProcessTitleChanged.fire(title); } public emitPid(pid: number): void { - this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess); + this._onProcessIdReady.fire(pid); } public emitExit(exitCode: number): void { - this.emit('exit', exitCode); + this._onProcessExit.fire(exitCode); this.dispose(); } - public send(message: IMessageToTerminalProcess): boolean { - switch (message.event) { - case 'input': this.emit('input', message.data); break; - case 'resize': this.emit('resize', message.cols, message.rows); break; - case 'shutdown': this.emit('shutdown'); break; - } - return true; + public shutdown(immediate: boolean): void { + this._onShutdown.fire(immediate); } - public onInput(listener: (data: string) => void): void { - const outerListener = (data) => listener(data); - this.on('input', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('input', outerListener))); + public input(data: string): void { + this._onInput.fire(data); } - public onResize(listener: (cols: number, rows: number) => void): void { - const outerListener = (cols, rows) => listener(cols, rows); - this.on('resize', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('resize', outerListener))); - } - - public onShutdown(listener: () => void): void { - const outerListener = () => listener(); - this.on('shutdown', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener))); + public resize(cols: number, rows: number): void { + this._onResize.fire({ cols, rows }); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts index 55e629b6feb..c26031468be 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts @@ -33,7 +33,7 @@ interface LinkFormatInfo { suite('Workbench - TerminalLinkHandler', () => { suite('localLinkRegex', () => { test('Windows', () => { - const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null, null, null, null, null); + const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null, null, null, null); function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) { assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl); assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl); @@ -103,7 +103,7 @@ suite('Workbench - TerminalLinkHandler', () => { }); test('Linux', () => { - const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null, null, null, null, null); + const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null, null, null, null); function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) { assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl); assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl); @@ -167,7 +167,8 @@ suite('Workbench - TerminalLinkHandler', () => { suite('preprocessPath', () => { test('Windows', () => { - const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, 'C:\\base', null, null, null, null); + const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null, null, null, null); + linkHandler.initialCwd = 'C:\\base'; let stub = sinon.stub(path, 'join', function (arg1: string, arg2: string) { return arg1 + '\\' + arg2; @@ -180,7 +181,8 @@ suite('Workbench - TerminalLinkHandler', () => { }); test('Linux', () => { - const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, '/base', null, null, null, null); + const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null, null, null, null); + linkHandler.initialCwd = '/base'; let stub = sinon.stub(path, 'join', function (arg1: string, arg2: string) { return arg1 + '/' + arg2; @@ -193,7 +195,7 @@ suite('Workbench - TerminalLinkHandler', () => { }); test('No Workspace', () => { - const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null, null, null, null, null); + const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null, null, null, null); assert.equal(linkHandler.preprocessPath('./src/file1'), null); assert.equal(linkHandler.preprocessPath('src/file2'), null); diff --git a/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts index 003f76b383b..495ffc45da2 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts @@ -4,19 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Terminal } from 'vscode-xterm'; +import { Terminal, TerminalCore } from 'vscode-xterm'; import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; import { isWindows } from 'vs/base/common/platform'; -interface TestTerminal extends Terminal { +interface TestTerminalCore extends TerminalCore { writeBuffer: string[]; _innerWrite(): void; } +interface TestTerminal extends Terminal { + _core: TestTerminalCore; +} + function syncWrite(term: TestTerminal, data: string): void { // Terminal.write is asynchronous - term.writeBuffer.push(data); - term._innerWrite(); + term._core.writeBuffer.push(data); + term._core._innerWrite(); } const ROWS = 10; @@ -62,24 +66,24 @@ suite('Workbench - TerminalCommandTracker', () => { for (let i = 0; i < 20; i++) { syncWrite(xterm, `\r\n`); } - assert.equal(xterm.buffer.ybase, 20); - assert.equal(xterm.buffer.ydisp, 20); + assert.equal(xterm._core.buffer.ybase, 20); + assert.equal(xterm._core.buffer.ydisp, 20); // Scroll to marker commandTracker.scrollToPreviousCommand(); - assert.equal(xterm.buffer.ydisp, 9); + assert.equal(xterm._core.buffer.ydisp, 9); // Scroll to top boundary commandTracker.scrollToPreviousCommand(); - assert.equal(xterm.buffer.ydisp, 0); + assert.equal(xterm._core.buffer.ydisp, 0); // Scroll to marker commandTracker.scrollToNextCommand(); - assert.equal(xterm.buffer.ydisp, 9); + assert.equal(xterm._core.buffer.ydisp, 9); // Scroll to bottom boundary commandTracker.scrollToNextCommand(); - assert.equal(xterm.buffer.ydisp, 20); + assert.equal(xterm._core.buffer.ydisp, 20); }); test('should select to the next and previous commands', () => { (window).matchMedia = () => { @@ -98,8 +102,8 @@ suite('Workbench - TerminalCommandTracker', () => { assert.equal(xterm.markers[1].line, 11); syncWrite(xterm, '\n\r3'); - assert.equal(xterm.buffer.ybase, 3); - assert.equal(xterm.buffer.ydisp, 3); + assert.equal(xterm._core.buffer.ybase, 3); + assert.equal(xterm._core.buffer.ydisp, 3); assert.equal(xterm.getSelection(), ''); commandTracker.selectToPreviousCommand(); @@ -128,8 +132,8 @@ suite('Workbench - TerminalCommandTracker', () => { assert.equal(xterm.markers[1].line, 11); syncWrite(xterm, '\n\r3'); - assert.equal(xterm.buffer.ybase, 3); - assert.equal(xterm.buffer.ydisp, 3); + assert.equal(xterm._core.buffer.ybase, 3); + assert.equal(xterm._core.buffer.ydisp, 3); assert.equal(xterm.getSelection(), ''); commandTracker.selectToPreviousLine(); 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 5fae93b3d01..fbb6acbfb40 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts @@ -7,47 +7,49 @@ import * as assert from 'assert'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; -import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; suite('Workbench - TerminalEnvironment', () => { - test('createTerminalEnv', function () { - const shell1 = { - executable: '/bin/foosh', - args: ['-bar', 'baz'] - }; - const parentEnv1: IStringDictionary = { - ok: true - } as any; - const env1 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au'); - assert.ok(env1['ok'], 'Parent environment is copied'); - assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged'); - assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID'); - assert.equal(env1['PTYSHELL'], '/bin/foosh', 'PTYSHELL is equal to the provided shell'); - assert.equal(env1['PTYSHELLARG0'], '-bar', 'PTYSHELLARG0 is equal to the first shell argument'); - assert.equal(env1['PTYSHELLARG1'], 'baz', 'PTYSHELLARG1 is equal to the first shell argument'); - assert.ok(!('PTYSHELLARG2' in env1), 'PTYSHELLARG2 is unset'); - assert.equal(env1['PTYCWD'], '/foo', 'PTYCWD is equal to requested cwd'); - assert.equal(env1['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + test('addTerminalEnvironmentKeys', () => { + const env = { FOO: 'bar' }; + const locale = 'en-au'; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + assert.equal(env['TERM_PROGRAM'], 'vscode'); + assert.equal(env['TERM_PROGRAM_VERSION'].search(/^\d+\.\d+\.\d+$/), 0); + assert.equal(env['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - const shell2: IShellLaunchConfig = { - executable: '/bin/foosh', - args: [] - }; - const parentEnv2: IStringDictionary = { - LANG: 'en_US.UTF-8' - }; - const env2 = terminalEnvironment.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au'); - assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset'); - assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo'); - assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + const env2 = { FOO: 'bar' }; + terminalEnvironment.addTerminalEnvironmentKeys(env2, null); + assert.equal(env2['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 - const env3 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/', null); - assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 + const env3 = { LANG: 'en_US.UTF-8' }; + terminalEnvironment.addTerminalEnvironmentKeys(env3, null); + assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + }); - const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null); - 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_12345: 'x' + }; + terminalEnvironment.sanitizeEnvironment(env); + assert.equal(env['FOO'], 'bar'); + assert.equal(Object.keys(env).length, 1); }); suite('mergeEnvironments', () => { diff --git a/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts b/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts index cc04c06a371..700deadc2cb 100644 --- a/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts +++ b/src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts @@ -10,10 +10,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; import { firstIndex } from 'vs/base/common/arrays'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { IWorkbenchThemeService, COLOR_THEME_SETTING, ICON_THEME_SETTING, IColorTheme, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { VIEWLET_ID, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -26,6 +25,8 @@ import { Color } from 'vs/base/common/color'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { schemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; export class SelectColorThemeAction extends Action { @@ -35,7 +36,7 @@ export class SelectColorThemeAction extends Action { constructor( id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService, @IViewletService private viewletService: IViewletService, @@ -48,15 +49,18 @@ export class SelectColorThemeAction extends Action { return this.themeService.getColorThemes().then(themes => { const currentTheme = this.themeService.getColorTheme(); - const picks: IPickOpenEntry[] = [].concat( + const picks: QuickPickInput[] = [].concat( toEntries(themes.filter(t => t.type === LIGHT), localize('themes.category.light', "light themes")), - toEntries(themes.filter(t => t.type === DARK), localize('themes.category.dark', "dark themes"), true), - toEntries(themes.filter(t => t.type === HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes"), true), - configurationEntries(this.extensionGalleryService, this.viewletService, 'category:themes', localize('installColorThemes', "Install Additional Color Themes...")) + toEntries(themes.filter(t => t.type === DARK), localize('themes.category.dark', "dark themes")), + toEntries(themes.filter(t => t.type === HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")), + configurationEntries(this.extensionGalleryService, localize('installColorThemes', "Install Additional Color Themes...")) ); const selectTheme = (theme, applyTheme: boolean) => { if (typeof theme.id === 'undefined') { // 'pick in marketplace' entry + if (applyTheme) { + openExtensionViewlet(this.viewletService, 'category:themes'); + } theme = currentTheme; } let target = null; @@ -65,23 +69,22 @@ export class SelectColorThemeAction extends Action { target = typeof confValue.workspace !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; } - this.themeService.setColorTheme(theme.id, target).done(null, + this.themeService.setColorTheme(theme.id, target).then(null, err => { + onUnexpectedError(err); this.themeService.setColorTheme(currentTheme.id, null); } ); }; const placeHolder = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)"); - const autoFocusIndex = firstIndex(picks, p => p.id === currentTheme.id); + const autoFocusIndex = firstIndex(picks, p => p.type !== 'separator' && p.id === currentTheme.id); const delayer = new Delayer(100); + const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); - return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex } }) - .then( - theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0), - null, - theme => delayer.trigger(() => selectTheme(theme, false)) - ); + return this.quickInputService.pick(picks, { placeHolder, activeItem: picks[autoFocusIndex], onDidFocus: tryTheme }) + .then(chooseTheme); }); } } @@ -94,7 +97,7 @@ class SelectIconThemeAction extends Action { constructor( id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, + @IQuickInputService private quickInputService: IQuickInputService, @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService, @IViewletService private viewletService: IViewletService, @@ -108,14 +111,17 @@ class SelectIconThemeAction extends Action { return this.themeService.getFileIconThemes().then(themes => { const currentTheme = this.themeService.getFileIconTheme(); - let picks: IPickOpenEntry[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }]; + let picks: QuickPickInput[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }]; picks = picks.concat( toEntries(themes), - configurationEntries(this.extensionGalleryService, this.viewletService, 'tag:icon-theme', localize('installIconThemes', "Install Additional File Icon Themes...")) + configurationEntries(this.extensionGalleryService, localize('installIconThemes', "Install Additional File Icon Themes...")) ); const selectTheme = (theme, applyTheme: boolean) => { if (typeof theme.id === 'undefined') { // 'pick in marketplace' entry + if (applyTheme) { + openExtensionViewlet(this.viewletService, 'tag:icon-theme'); + } theme = currentTheme; } let target = null; @@ -123,49 +129,55 @@ class SelectIconThemeAction extends Action { let confValue = this.configurationService.inspect(ICON_THEME_SETTING); target = typeof confValue.workspace !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; } - this.themeService.setFileIconTheme(theme && theme.id, target).done(null, + this.themeService.setFileIconTheme(theme && theme.id, target).then(null, err => { + onUnexpectedError(err); this.themeService.setFileIconTheme(currentTheme.id, null); } ); }; const placeHolder = localize('themes.selectIconTheme', "Select File Icon Theme"); - const autoFocusIndex = firstIndex(picks, p => p.id === currentTheme.id); + const autoFocusIndex = firstIndex(picks, p => p.type !== 'separator' && p.id === currentTheme.id); const delayer = new Delayer(100); + const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); - return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex } }) - .then( - theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0), - null, - theme => delayer.trigger(() => selectTheme(theme, false)) - ); + return this.quickInputService.pick(picks, { placeHolder, activeItem: picks[autoFocusIndex], onDidFocus: tryTheme }) + .then(chooseTheme); }); } } -function configurationEntries(extensionGalleryService: IExtensionGalleryService, viewletService: IViewletService, query: string, label: string): IPickOpenEntry[] { +function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string): QuickPickInput[] { if (extensionGalleryService.isEnabled()) { - return [{ - id: void 0, - label: label, - separator: { border: true }, - alwaysShow: true, - run: () => viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { - (viewlet).search(query); - viewlet.focus(); - }) - }]; + return [ + { + type: 'separator' + }, + { + id: void 0, + label: label, + alwaysShow: true, + } + ]; } return []; } -function toEntries(themes: (IColorTheme | IFileIconTheme)[], label?: string, border = false) { - const toEntry = theme => { id: theme.id, label: theme.label, description: theme.description }; - const sorter = (t1: IColorTheme, t2: IColorTheme) => t1.label.localeCompare(t2.label); - let entries = themes.map(toEntry).sort(sorter); - if (entries.length > 0 && (label || border)) { - entries[0].separator = { label, border }; +function openExtensionViewlet(viewletService: IViewletService, query: string) { + return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { + (viewlet).search(query); + viewlet.focus(); + }); +} + +function toEntries(themes: (IColorTheme | IFileIconTheme)[], label?: string) { + const toEntry = theme => { id: theme.id, label: theme.label, description: theme.description }; + const sorter = (t1: IQuickPickItem, t2: IQuickPickItem) => t1.label.localeCompare(t2.label); + let entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); + if (entries.length > 0 && label) { + entries.unshift({ type: 'separator', label }); } return entries; } @@ -229,3 +241,21 @@ const developerCategory = localize('developer', "Developer"); const generateColorThemeDescriptor = new SyncActionDescriptor(GenerateColorThemeAction, GenerateColorThemeAction.ID, GenerateColorThemeAction.LABEL); Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(generateColorThemeDescriptor, 'Developer: Generate Color Theme From Current Settings', developerCategory); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: SelectColorThemeAction.ID, + title: localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme") + }, + order: 1 +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: SelectIconThemeAction.ID, + title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme") + }, + order: 2 +}); diff --git a/src/vs/workbench/parts/themes/test/electron-browser/themes.test.contribution.ts b/src/vs/workbench/parts/themes/test/electron-browser/themes.test.contribution.ts index 871bbc06dce..5ed96250e86 100644 --- a/src/vs/workbench/parts/themes/test/electron-browser/themes.test.contribution.ts +++ b/src/vs/workbench/parts/themes/test/electron-browser/themes.test.contribution.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as paths from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as pfs from 'vs/base/node/pfs'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/parts/update/electron-browser/media/update.contribution.css b/src/vs/workbench/parts/update/electron-browser/media/update.contribution.css index 4e602f932a2..54537cdcfa7 100644 --- a/src/vs/workbench/parts/update/electron-browser/media/update.contribution.css +++ b/src/vs/workbench/parts/update/electron-browser/media/update.contribution.css @@ -7,9 +7,3 @@ -webkit-mask: url('update.svg') no-repeat 50% 50%; -webkit-mask-size: 22px; } - -/* TODO@Ben this is a hack to overwrite the icon for release notes eitor */ -.file-icons-enabled .show-file-icons .release-notes-ext-file-icon.file-icon::before { - content: ' '; - background-image: url('code-icon.svg'); -} \ No newline at end of file diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index f1dcc32c086..88110f40e35 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { marked } from 'vs/base/common/marked/marked'; import { OS } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { asText } from 'vs/base/node/request'; import { IMode, TokenizationRegistry } from 'vs/editor/common/modes'; @@ -25,8 +25,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; import { IWebviewEditorService } from 'vs/workbench/parts/webview/electron-browser/webviewEditorService'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; +import { CancellationToken } from 'vs/base/common/cancellation'; function renderBody( body: string, @@ -51,6 +52,7 @@ export class ReleaseNotesManager { private _releaseNotesCache: { [version: string]: TPromise; } = Object.create(null); private _currentReleaseNotes: WebviewEditorInput | undefined = undefined; + private _lastText: string; public constructor( @IEnvironmentService private readonly _environmentService: IEnvironmentService, @@ -60,14 +62,25 @@ export class ReleaseNotesManager { @IRequestService private readonly _requestService: IRequestService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IEditorService private readonly _editorService: IEditorService, - @IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService, - ) { } + @IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService + ) { + TokenizationRegistry.onDidChange(async () => { + if (!this._currentReleaseNotes || !this._lastText) { + return; + } + const html = await this.renderBody(this._lastText); + if (this._currentReleaseNotes) { + this._currentReleaseNotes.html = html; + } + }); + } public async show( accessor: ServicesAccessor, version: string - ): TPromise { + ): Promise { const releaseNoteText = await this.loadReleaseNotes(version); + this._lastText = releaseNoteText; const html = await this.renderBody(releaseNoteText); const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version); @@ -87,6 +100,11 @@ export class ReleaseNotesManager { onDispose: () => { this._currentReleaseNotes = undefined; } }); + const iconPath = URI.parse(require.toUrl('./media/code-icon.svg')); + this._currentReleaseNotes.iconPath = { + light: iconPath, + dark: iconPath + }; this._currentReleaseNotes.html = html; } @@ -118,7 +136,7 @@ export class ReleaseNotesManager { }; const kbstyle = (match: string, kb: string) => { - const keybinding = KeybindingIO.readKeybinding(kb, OS); + const keybinding = KeybindingParser.parseKeybinding(kb, OS); if (!keybinding) { return unassigned; @@ -139,8 +157,15 @@ export class ReleaseNotesManager { }; if (!this._releaseNotesCache[version]) { - this._releaseNotesCache[version] = this._requestService.request({ url }) + this._releaseNotesCache[version] = this._requestService.request({ url }, CancellationToken.None) .then(asText) + .then(text => { + if (!/^#\s/.test(text)) { // release notes always starts with `#` followed by whitespace + return TPromise.wrapError(new Error('Invalid release notes')); + } + + return TPromise.wrap(text); + }) .then(text => patchKeybindings(text)); } @@ -154,13 +179,14 @@ export class ReleaseNotesManager { } private async renderBody(text: string) { + const content = await this.renderContent(text); const colorMap = TokenizationRegistry.getColorMap(); const css = generateTokensCSSForColorMap(colorMap); - const body = renderBody(await this.renderContent(text), css); + const body = renderBody(content, css); return body; } - private async renderContent(text: string): TPromise { + private async renderContent(text: string): Promise { const renderer = await this.getRenderer(text); return marked(text, { renderer }); } diff --git a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts index 1a2d956f296..4e791267124 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts @@ -16,12 +16,14 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, Win3264BitContribution } from './update'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running); +const workbench = Registry.as(WorkbenchExtensions.Workbench); -if (platform.isWindows && process.arch === 'ia32') { - Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(Win3264BitContribution, LifecyclePhase.Running); +workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Running); + +if (platform.isWindows) { + if (process.arch === 'ia32') { + workbench.registerWorkbenchContribution(Win3264BitContribution, LifecyclePhase.Running); + } } Registry.as(GlobalActivityExtensions) diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index 4f8ed441d1e..53ba1b49137 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -9,11 +9,11 @@ import * as nls from 'vs/nls'; import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; -import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IActivityService, NumberBadge, IBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IGlobalActivity } from 'vs/workbench/common/activity'; @@ -73,11 +73,11 @@ export abstract class AbstractShowReleaseNotesAction extends Action { this.enabled = false; - return showReleaseNotes(this.instantiationService, this.version) + return TPromise.wrap(showReleaseNotes(this.instantiationService, this.version) .then(null, () => { const action = this.instantiationService.createInstance(OpenLatestReleaseNotesInBrowserAction); return action.run().then(() => false); - }); + })); } } @@ -235,7 +235,7 @@ export class UpdateContribution implements IGlobalActivity { get cssClass() { return 'update-activity'; } private state: UpdateState; - private badgeDisposable: IDisposable = EmptyDisposable; + private badgeDisposable: IDisposable = Disposable.None; private disposables: IDisposable[] = []; constructor( @@ -349,7 +349,7 @@ export class UpdateContribution implements IGlobalActivity { ); } - // windows fast updates + // windows fast updates (target === system) private onUpdateDownloaded(update: IUpdate): void { if (!this.shouldShowNotification()) { return; @@ -377,6 +377,11 @@ export class UpdateContribution implements IGlobalActivity { // windows fast updates private onUpdateUpdating(update: IUpdate): void { + if (isWindows && product.target === 'user') { + return; + } + + // windows fast updates (target === system) const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); if (!neverShowAgain.shouldShow()) { @@ -399,10 +404,11 @@ export class UpdateContribution implements IGlobalActivity { // windows and mac private onUpdateReady(update: IUpdate): void { - if (!isWindows && !this.shouldShowNotification()) { + if (!(isWindows && product.target !== 'user') && !this.shouldShowNotification()) { return; } + // windows user fast updates and mac this.notificationService.prompt( severity.Info, nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", product.nameLong), @@ -445,8 +451,8 @@ export class UpdateContribution implements IGlobalActivity { new CommandAction(UpdateContribution.showCommandsId, nls.localize('commandPalette', "Command Palette..."), this.commandService), new Separator(), new CommandAction(UpdateContribution.openSettingsId, nls.localize('settings', "Settings"), this.commandService), + new CommandAction(UpdateContribution.showExtensionsId, nls.localize('showExtensions', "Extensions"), this.commandService), new CommandAction(UpdateContribution.openKeybindingsId, nls.localize('keyboardShortcuts', "Keyboard Shortcuts"), this.commandService), - new CommandAction(UpdateContribution.showExtensionsId, nls.localize('showExtensions', "Manage Extensions"), this.commandService), new Separator(), new CommandAction(UpdateContribution.openUserSnippets, nls.localize('userSnippets', "User Snippets"), this.commandService), new Separator(), @@ -501,4 +507,4 @@ export class UpdateContribution implements IGlobalActivity { dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/url/electron-browser/url.contribution.ts b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts index bc6c1f8faa3..a8f0ee32940 100644 --- a/src/vs/workbench/parts/url/electron-browser/url.contribution.ts +++ b/src/vs/workbench/parts/url/electron-browser/url.contribution.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { IURLService } from 'vs/platform/url/common/url'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; @@ -27,11 +27,11 @@ export class OpenUrlAction extends Action { super(id, label); } - async run(): TPromise { - const input = await this.quickInputService.input({ prompt: 'URL to open' }); - const uri = URI.parse(input); - - this.urlService.open(uri); + run(): TPromise { + return this.quickInputService.input({ prompt: 'URL to open' }).then(input => { + const uri = URI.parse(input); + this.urlService.open(uri); + }); } } diff --git a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts index 44449b32df2..61e9c768aa6 100644 --- a/src/vs/workbench/parts/watermark/electron-browser/watermark.ts +++ b/src/vs/workbench/parts/watermark/electron-browser/watermark.ts @@ -5,7 +5,6 @@ 'use strict'; import 'vs/css!./watermark'; -import { $, Builder } from 'vs/base/browser/builder'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { isMacintosh } from 'vs/base/common/platform'; @@ -24,9 +23,12 @@ import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/comm import { Parts, IPartService, IDimension } from 'vs/workbench/services/part/common/partService'; import { StartAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { FindInFilesActionId } from 'vs/workbench/parts/search/common/constants'; -import { ToggleTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { escape } from 'vs/base/common/strings'; import { QUICKOPEN_ACTION_ID } from 'vs/workbench/browser/parts/quickopen/quickopen'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/parts/terminal/common/terminalCommands'; +import * as dom from 'vs/base/browser/dom'; + +const $ = dom.$; interface WatermarkEntry { text: string; @@ -68,7 +70,7 @@ const newUntitledFile: WatermarkEntry = { const newUntitledFileMacOnly: WatermarkEntry = assign({ mac: true }, newUntitledFile); const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), - ids: [ToggleTerminalAction.ID] + ids: [TERMINAL_COMMAND_ID.TOGGLE] }; const findInFiles: WatermarkEntry = { @@ -104,7 +106,7 @@ const WORKBENCH_TIPS_ENABLED_KEY = 'workbench.tips.enabled'; export class WatermarkContribution implements IWorkbenchContribution { private toDispose: IDisposable[] = []; - private watermark: Builder; + private watermark: HTMLElement; private enabled: boolean; private workbenchState: WorkbenchState; @@ -149,35 +151,31 @@ export class WatermarkContribution implements IWorkbenchContribution { const container = this.partService.getContainer(Parts.EDITOR_PART); container.classList.add('has-watermark'); - this.watermark = $() - .div({ 'class': 'watermark' }); - const box = $(this.watermark) - .div({ 'class': 'watermark-box' }); + this.watermark = $('.watermark'); + const box = dom.append(this.watermark, $('.watermark-box')); const folder = this.workbenchState !== WorkbenchState.EMPTY; const selected = folder ? folderEntries : noFolderEntries .filter(entry => !('mac' in entry) || entry.mac === isMacintosh); const update = () => { - const builder = $(box); - builder.clearChildren(); + dom.clearNode(box); selected.map(entry => { - builder.element('dl', {}, dl => { - dl.element('dt', {}, dt => dt.text(entry.text)); - dl.element('dd', {}, dd => dd.innerHtml( - entry.ids - .map(id => { - let k = this.keybindingService.lookupKeybinding(id); - if (k) { - return `${escape(k.getLabel())}`; - } - return `${escape(UNBOUND)}`; - }) - .join(' / ') - )); - }); + const dl = dom.append(box, $('dl')); + const dt = dom.append(dl, $('dt')); + dt.textContent = entry.text; + const dd = dom.append(dl, $('dd')); + dd.innerHTML = entry.ids + .map(id => { + let k = this.keybindingService.lookupKeybinding(id); + if (k) { + return `${escape(k.getLabel())}`; + } + return `${escape(UNBOUND)}`; + }) + .join(' / '); }); }; update(); - this.watermark.build(container.firstElementChild as HTMLElement, 0); + dom.prepend(container.firstElementChild as HTMLElement, this.watermark); this.toDispose.push(this.keybindingService.onDidUpdateKeybindings(update)); this.toDispose.push(this.partService.onEditorLayout(({ height }: IDimension) => { container.classList[height <= 478 ? 'add' : 'remove']('max-height-478px'); @@ -186,7 +184,7 @@ export class WatermarkContribution implements IWorkbenchContribution { private destroy(): void { if (this.watermark) { - this.watermark.destroy(); + this.watermark.remove(); this.partService.getContainer(Parts.EDITOR_PART).classList.remove('has-watermark'); this.dispose(); } diff --git a/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts index 8436046783d..66bc0a66e90 100644 --- a/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/baseWebviewEditor.ts @@ -10,10 +10,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { WebviewElement } from './webviewElement'; -/** A context key that is set when a webview editor has focus. */ -export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS = new RawContextKey('webviewEditorFocus', false); -/** A context key that is set when the find widget find input in webview editor webview is focused. */ -export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey('webviewEditorFindWidgetInputFocused', false); /** A context key that is set when the find widget in a webview is visible. */ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); @@ -24,9 +20,7 @@ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey< export abstract class BaseWebviewEditor extends BaseEditor { protected _webview: WebviewElement | undefined; - protected contextKey: IContextKey; protected findWidgetVisible: IContextKey; - protected findInputFocusContextKey: IContextKey; constructor( id: string, @@ -36,8 +30,6 @@ export abstract class BaseWebviewEditor extends BaseEditor { ) { super(id, telemetryService, themeService); if (contextKeyService) { - this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(contextKeyService); - this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(contextKeyService); this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(contextKeyService); } } @@ -56,18 +48,6 @@ export abstract class BaseWebviewEditor extends BaseEditor { } } - public showNextFindTerm() { - if (this._webview) { - this._webview.showNextFindTerm(); - } - } - - public showPreviousFindTerm() { - if (this._webview) { - this._webview.showPreviousFindTerm(); - } - } - public get isWebviewEditor() { return true; } @@ -89,4 +69,10 @@ export abstract class BaseWebviewEditor extends BaseEditor { this._webview.focus(); } } + + public selectAll(): void { + if (this._webview) { + this._webview.selectAll(); + } + } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js index 51a9d42aa32..17e1b20b3f4 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js +++ b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js @@ -99,7 +99,7 @@ return; } - const progress = event.target.body.scrollTop / event.target.body.clientHeight; + const progress = event.currentTarget.scrollY / event.target.body.clientHeight; if (isNaN(progress)) { return; } @@ -172,7 +172,7 @@ } // apply default script - if (enableWrappedPostMessage) { + if (enableWrappedPostMessage && options.allowScripts) { const defaultScript = newDocument.createElement('script'); defaultScript.textContent = ` const acquireVsCodeApi = (function() { @@ -312,18 +312,18 @@ var setInitialScrollPosition; if (firstLoad) { firstLoad = false; - setInitialScrollPosition = (body) => { + setInitialScrollPosition = (body, window) => { if (!isNaN(initData.initialScrollProgress)) { if (body.scrollTop === 0) { - body.scrollTop = body.clientHeight * initData.initialScrollProgress; + window.scroll(0, body.clientHeight * initData.initialScrollProgress); } } }; } else { const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentDocument.body.scrollTop : 0; - setInitialScrollPosition = (body) => { + setInitialScrollPosition = (body, window) => { if (body.scrollTop === 0) { - body.scrollTop = scrollY; + window.scroll(0, scrollY); } }; } @@ -360,7 +360,7 @@ if (contentDocument.body) { // Workaround for https://github.com/Microsoft/vscode/issues/12865 // check new scrollTop and reset if neccessary - setInitialScrollPosition(contentDocument.body); + setInitialScrollPosition(contentDocument.body, contentWindow); // Bubble out link clicks contentDocument.body.addEventListener('click', handleInnerClick); 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 c455d333963..cfea3a0851c 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts @@ -9,14 +9,14 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { WebviewEditorInputFactory } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory'; -import { KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; -import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindTermCommand, ShowWebViewEditorFindWidgetCommand } from './webviewCommands'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; +import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand } from './webviewCommands'; import { WebviewEditor } from './webviewEditor'; import { WebviewEditorInput } from './webviewEditorInput'; import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService'; @@ -38,46 +38,43 @@ const webviewDeveloperCategory = localize('developer', "Developer"); const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); -const showNextFindWdigetCommand = new ShowWebViewEditorFindWidgetCommand({ - id: ShowWebViewEditorFindWidgetCommand.ID, - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindWdigetCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); +export function registerWebViewCommands(editorId: string): void { + const contextKeyExpr = ContextKeyExpr.equals('activeEditor', editorId); + const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({ + id: ShowWebViewEditorFindWidgetCommand.ID, + precondition: contextKeyExpr, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + weight: KeybindingWeight.EditorContrib + } + }); + showNextFindWidgetCommand.register(); -const showNextFindTermCommand = new ShowWebViewEditorFindTermCommand({ - id: 'editor.action.webvieweditor.showNextFindTerm', - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.DownArrow - } -}, true); -KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + const hideCommand = new HideWebViewEditorFindCommand({ + id: HideWebViewEditorFindCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), + kbOpts: { + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib + } + }); + hideCommand.register(); -const showPreviousFindTermCommand = new ShowWebViewEditorFindTermCommand({ - id: 'editor.action.webvieweditor.showPreviousFindTerm', - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, - kbOpts: { - primary: KeyMod.Alt | KeyCode.UpArrow - } -}, false); -KeybindingsRegistry.registerCommandAndKeybindingRule(showPreviousFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); - - -const hideCommand = new HideWebViewEditorFindCommand({ - id: HideWebViewEditorFindCommand.ID, - precondition: ContextKeyExpr.and( - KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, - KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), - kbOpts: { - primary: KeyCode.Escape - } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(hideCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + const selectAllCommand = new SelectAllWebviewEditorCommand({ + id: SelectAllWebviewEditorCommand.ID, + precondition: contextKeyExpr, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_A, + weight: KeybindingWeight.EditorContrib + } + }); + selectAllCommand.register(); +} +registerWebViewCommands(WebviewEditor.ID); actionRegistry.registerWorkbenchAction( new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL), diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts index 8655aa91f32..5866fcdbec6 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewCommands.ts @@ -5,7 +5,7 @@ import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; -import { Command, ICommandOptions } from 'vs/editor/browser/editorExtensions'; +import { Command } from 'vs/editor/browser/editorExtensions'; import * as nls from 'vs/nls'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -33,21 +33,13 @@ export class HideWebViewEditorFindCommand extends Command { } } -export class ShowWebViewEditorFindTermCommand extends Command { - public static readonly Id = 'editor.action.webvieweditor.showPreviousFindTerm'; - - constructor(opts: ICommandOptions, private _next: boolean) { - super(opts); - } +export class SelectAllWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.selectAll'; public runCommand(accessor: ServicesAccessor, args: any): void { const webViewEditor = getActiveWebviewEditor(accessor); if (webViewEditor) { - if (this._next) { - webViewEditor.showNextFindTerm(); - } else { - webViewEditor.showPreviousFindTerm(); - } + webViewEditor.selectAll(); } } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts index 66bf601c159..95ca32be767 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts @@ -4,24 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { domEvent } from 'vs/base/browser/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { URI } from 'vs/base/common/uri'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; -import { BaseWebviewEditor, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; +import { BaseWebviewEditor, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './baseWebviewEditor'; import { WebviewElement } from './webviewElement'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWindowService } from 'vs/platform/windows/common/windows'; export class WebviewEditor extends BaseWebviewEditor { @@ -44,7 +43,8 @@ export class WebviewEditor extends BaseWebviewEditor { @IPartService private readonly _partService: IPartService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService + @IEditorService private readonly _editorService: IEditorService, + @IWindowService private readonly _windowService: IWindowService ) { super(WebviewEditor.ID, telemetryService, themeService, _contextKeyService); } @@ -83,8 +83,8 @@ export class WebviewEditor extends BaseWebviewEditor { } // Make sure we restore focus when switching back to a VS Code window - this._onFocusWindowHandler = domEvent(window, 'focus')(() => { - if (this._editorService.activeControl === this) { + this._onFocusWindowHandler = this._windowService.onDidChangeFocus(focused => { + if (focused && this._editorService.activeControl === this) { this.focus(); } }); @@ -95,14 +95,21 @@ export class WebviewEditor extends BaseWebviewEditor { this._webview = undefined; this._webviewContent = undefined; + if (this._content && this._content.parentElement) { + this._content.parentElement.removeChild(this._content); + this._content = undefined; + } + this._onDidFocusWebview.dispose(); if (this._webviewFocusTracker) { this._webviewFocusTracker.dispose(); + this._webviewFocusTracker = undefined; } if (this._webviewFocusListenerDisposable) { this._webviewFocusListenerDisposable.dispose(); + this._webviewFocusListenerDisposable = undefined; } if (this._onFocusWindowHandler) { @@ -156,35 +163,34 @@ export class WebviewEditor extends BaseWebviewEditor { super.clearInput(); } - async setInput(input: WebviewEditorInput, options: EditorOptions, token: CancellationToken): TPromise { + setInput(input: WebviewEditorInput, options: EditorOptions, token: CancellationToken): Thenable { if (this.input) { (this.input as WebviewEditorInput).releaseWebview(this); this._webview = undefined; this._webviewContent = undefined; } - await super.setInput(input, options, token); + return super.setInput(input, options, token) + .then(() => input.resolve()) + .then(() => { + if (token.isCancellationRequested) { + return; + } - await input.resolve(); - - if (token.isCancellationRequested) { - return; - } - - input.updateGroup(this.group.id); - this.updateWebview(input); + input.updateGroup(this.group.id); + this.updateWebview(input); + }); } private updateWebview(input: WebviewEditorInput) { const webview = this.getWebview(input); input.claimWebview(this); - webview.options = { + webview.update(input.html, { allowScripts: input.options.enableScripts, allowSvgs: true, enableWrappedPostMessage: true, useSameOriginForRoot: false, localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots() - }; - input.html = input.html; + }); if (this._webviewContent) { this._webviewContent.style.visibility = 'visible'; @@ -217,16 +223,12 @@ export class WebviewEditor extends BaseWebviewEditor { } if (input.options.enableFindWidget) { - this._contextKeyService = this._contextKeyService.createScoped(this._webviewContent); - this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(this._contextKeyService); - this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService); + this._contextKeyService = this._register(this._contextKeyService.createScoped(this._webviewContent)); this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); } this._webview = this._instantiationService.createInstance(WebviewElement, this._partService.getContainer(Parts.EDITOR_PART), - this.contextKey, - this.findInputFocusContextKey, { enableWrappedPostMessage: true, useSameOriginForRoot: false diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts index 5959e91831a..7a6532cff51 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInput.ts @@ -2,13 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import * as dom from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { EditorInput, EditorModel, IEditorInput, GroupIdentifier } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import * as vscode from 'vscode'; import { WebviewEvents, WebviewInputOptions, WebviewReviver } from './webviewEditorService'; import { WebviewElement } from './webviewElement'; @@ -16,9 +18,42 @@ import { WebviewElement } from './webviewElement'; export class WebviewEditorInput extends EditorInput { private static handlePool = 0; + private static _styleElement?: HTMLStyleElement; + + private static _icons = new Map(); + + private static updateStyleElement( + id: number, + iconPath: { light: URI, dark: URI } | undefined + ) { + if (!this._styleElement) { + this._styleElement = dom.createStyleSheet(); + this._styleElement.className = 'webview-icons'; + } + + if (!iconPath) { + this._icons.delete(id); + } else { + this._icons.set(id, iconPath); + } + + const cssRules: string[] = []; + this._icons.forEach((value, key) => { + const webviewSelector = `.show-file-icons .webview-${key}-name-file-icon::before`; + if (URI.isUri(value)) { + cssRules.push(`${webviewSelector} { content: ""; background-image: url(${value.toString()}); }`); + } else { + cssRules.push(`${webviewSelector} { content: ""; background-image: url(${value.light.toString()}); }`); + cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: url(${value.dark.toString()}); }`); + } + }); + this._styleElement.innerHTML = cssRules.join('\n'); + } + public static readonly typeId = 'workbench.editors.webviewInput'; private _name: string; + private _iconPath?: { light: URI, dark: URI }; private _options: WebviewInputOptions; private _html: string = ''; private _currentWebviewHtml: string = ''; @@ -30,14 +65,15 @@ export class WebviewEditorInput extends EditorInput { private _group?: GroupIdentifier; private _scrollYPercentage: number = 0; private _state: any; - private _webviewState: string | undefined; private _revived: boolean = false; public readonly extensionLocation: URI | undefined; + private readonly _id: number; constructor( public readonly viewType: string, + id: number | undefined, name: string, options: WebviewInputOptions, state: any, @@ -47,6 +83,14 @@ export class WebviewEditorInput extends EditorInput { @IPartService private readonly _partService: IPartService, ) { super(); + + if (typeof id === 'number') { + this._id = id; + WebviewEditorInput.handlePool = Math.max(id, WebviewEditorInput.handlePool) + 1; + } else { + this._id = WebviewEditorInput.handlePool++; + } + this._name = name; this._options = options; this._events = events; @@ -58,6 +102,13 @@ export class WebviewEditorInput extends EditorInput { return WebviewEditorInput.typeId; } + public getId(): number { + return this._id; + } + + private readonly _onDidChangeIcon = this._register(new Emitter()); + public readonly onDidChangeIcon = this._onDidChangeIcon.event; + public dispose() { this.disposeWebview(); @@ -71,11 +122,15 @@ export class WebviewEditorInput extends EditorInput { } this._events = undefined; + this._webview = undefined; super.dispose(); } public getResource(): URI { - return null; + return URI.from({ + scheme: 'webview-panel', + path: `webview-panel/webview-${this._id}` + }); } public getName(): string { @@ -95,8 +150,17 @@ export class WebviewEditorInput extends EditorInput { this._onDidChangeLabel.fire(); } - matches(other: IEditorInput): boolean { - return other && other === this; + public get iconPath() { + return this._iconPath; + } + + public set iconPath(value: { light: URI, dark: URI } | undefined) { + this._iconPath = value; + WebviewEditorInput.updateStyleElement(this._id, value); + } + + public matches(other: IEditorInput): boolean { + return other === this || (other instanceof WebviewEditorInput && other._id === this._id); } public get group(): GroupIdentifier | undefined { @@ -129,18 +193,31 @@ export class WebviewEditorInput extends EditorInput { } public get webviewState() { - return this._webviewState; + return this._state.state; } public get options(): WebviewInputOptions { return this._options; } - public set options(value: WebviewInputOptions) { - this._options = value; + public setOptions(value: vscode.WebviewOptions) { + this._options = { + ...this._options, + ...value + }; + + if (this._webview) { + this._webview.options = { + allowScripts: this._options.enableScripts, + allowSvgs: true, + enableWrappedPostMessage: true, + useSameOriginForRoot: false, + localResourceRoots: this._options.localResourceRoots + }; + } } - public resolve(refresh?: boolean): TPromise { + public resolve(): TPromise { if (this.reviver && !this._revived) { this._revived = true; return this.reviver.reviveWebview(this).then(() => new EditorModel()); @@ -155,9 +232,8 @@ export class WebviewEditorInput extends EditorInput { public get container(): HTMLElement { if (!this._container) { - const id = WebviewEditorInput.handlePool++; this._container = document.createElement('div'); - this._container.id = `webview-${id}`; + this._container.id = `webview-${this._id}`; this._partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); } return this._container; @@ -189,7 +265,7 @@ export class WebviewEditorInput extends EditorInput { }, null, this._webviewDisposables); this._webview.onDidUpdateState(newState => { - this._webviewState = newState; + this._state.state = newState; }, null, this._webviewDisposables); } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts index 094c71d62a2..8cc701c66d8 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorInputFactory.ts @@ -7,18 +7,16 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEditorInputFactory } from 'vs/workbench/common/editor'; import { WebviewEditorInput } from './webviewEditorInput'; import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; interface SerializedWebview { readonly viewType: string; + readonly id: number; readonly title: string; readonly options: WebviewInputOptions; - /** - * compatibility with previous versions - */ - readonly extensionFolderPath?: string; readonly extensionLocation: string; readonly state: any; + readonly iconPath: { light: string, dark: string } | undefined; } export class WebviewEditorInputFactory implements IEditorInputFactory { @@ -44,27 +42,23 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { const data: SerializedWebview = { viewType: input.viewType, + id: input.getId(), title: input.getName(), options: input.options, extensionLocation: input.extensionLocation.toString(), - state: input.state + state: input.state, + iconPath: input.iconPath ? { light: input.iconPath.light.toString(), dark: input.iconPath.dark.toString(), } : undefined, }; return JSON.stringify(data); } public deserialize( - instantiationService: IInstantiationService, + _instantiationService: IInstantiationService, serializedEditorInput: string ): WebviewEditorInput { const data: SerializedWebview = JSON.parse(serializedEditorInput); - let extensionLocation: URI; - if (typeof data.extensionLocation === 'string') { - extensionLocation = URI.parse(data.extensionLocation); - } - if (typeof data.extensionFolderPath === 'string') { - // compatibility with previous versions - extensionLocation = URI.file(data.extensionFolderPath); - } - return this._webviewService.reviveWebview(data.viewType, data.title, data.state, data.options, extensionLocation); + const extensionLocation = URI.parse(data.extensionLocation); + const iconPath = data.iconPath ? { light: URI.parse(data.iconPath.light), dark: URI.parse(data.iconPath.dark) } : undefined; + return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation); } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts index 15796fb1219..5824d631de6 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditorService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; @@ -12,6 +12,7 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/ import * as vscode from 'vscode'; import { WebviewEditorInput } from './webviewEditorInput'; import { GroupIdentifier } from 'vs/workbench/common/editor'; +import { equals } from 'vs/base/common/arrays'; export const IWebviewEditorService = createDecorator('webviewEditorService'); @@ -34,7 +35,9 @@ export interface IWebviewEditorService { reviveWebview( viewType: string, + id: number, title: string, + iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, extensionLocation: URI @@ -76,6 +79,15 @@ export interface WebviewInputOptions extends vscode.WebviewOptions, vscode.Webvi tryRestoreScrollPosition?: boolean; } +export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewInputOptions): boolean { + return a.enableCommandUris === b.enableCommandUris + && a.enableFindWidget === b.enableFindWidget + && a.enableScripts === b.enableScripts + && a.retainContextWhenHidden === b.retainContextWhenHidden + && a.tryRestoreScrollPosition === b.tryRestoreScrollPosition + && (a.localResourceRoots === b.localResourceRoots || (Array.isArray(a.localResourceRoots) && Array.isArray(b.localResourceRoots) && equals(a.localResourceRoots, b.localResourceRoots, (a, b) => a.toString() === b.toString()))); +} + export class WebviewEditorService implements IWebviewEditorService { _serviceBrand: any; @@ -96,7 +108,7 @@ export class WebviewEditorService implements IWebviewEditorService { extensionLocation: URI, events: WebviewEvents ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extensionLocation, undefined); + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extensionLocation, undefined); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group); return webviewInput; } @@ -115,28 +127,32 @@ export class WebviewEditorService implements IWebviewEditorService { reviveWebview( viewType: string, + id: number, title: string, + iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, extensionLocation: URI ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, state, {}, extensionLocation, { + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, id, title, options, state, {}, extensionLocation, { canRevive: (_webview) => { return true; }, - reviveWebview: async (webview: WebviewEditorInput): TPromise => { - const didRevive = await this.tryRevive(webview); - if (didRevive) { - return; - } - // A reviver may not be registered yet. Put into queue and resolve promise when we can revive - let resolve: (value: void) => void; - const promise = new TPromise(r => { resolve = r; }); - this._awaitingRevival.push({ input: webview, resolve }); - return promise; + reviveWebview: (webview: WebviewEditorInput): TPromise => { + return TPromise.wrap(this.tryRevive(webview)).then(didRevive => { + if (didRevive) { + return TPromise.as(void 0); + } + + // A reviver may not be registered yet. Put into queue and resolve promise when we can revive + let resolve: (value: void) => void; + const promise = new TPromise(r => { resolve = r; }); + this._awaitingRevival.push({ input: webview, resolve }); + return promise; + }); } }); - + webviewInput.iconPath = iconPath; return webviewInput; } @@ -173,7 +189,7 @@ export class WebviewEditorService implements IWebviewEditorService { private async tryRevive( webview: WebviewEditorInput - ): TPromise { + ): Promise { const revivers = this._revivers.get(webview.viewType); if (!revivers) { return false; @@ -187,4 +203,4 @@ export class WebviewEditorService implements IWebviewEditorService { } return false; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts index 4cc5af9bf5b..898a782901f 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewElement.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { addClass, addDisposableListener } from 'vs/base/browser/dom'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { getMediaMime, guessMimeTypes } from 'vs/base/common/mime'; -import { nativeSep, extname } from 'vs/base/common/paths'; -import { startsWith } from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; +import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/parts/webview/electron-browser/webviewProtocols'; +import { areWebviewInputOptionsEqual } from './webviewEditorService'; import { WebviewFindWidget } from './webviewFindWidget'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export interface WebviewOptions { readonly allowScripts?: boolean; @@ -27,10 +25,9 @@ export interface WebviewOptions { readonly localResourceRoots?: ReadonlyArray; } -export class WebviewElement { - private readonly _webview: Electron.WebviewTag; +export class WebviewElement extends Disposable { + private _webview: Electron.WebviewTag; private _ready: Promise; - private _disposables: IDisposable[] = []; private _webviewFindWidget: WebviewFindWidget; private _findStarted: boolean = false; @@ -39,20 +36,16 @@ export class WebviewElement { constructor( private readonly _styleElement: Element, - private readonly _contextKey: IContextKey, - private readonly _findInputContextKey: IContextKey, private _options: WebviewOptions, + @IInstantiationService instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IFileService private readonly _fileService: IFileService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { + super(); this._webview = document.createElement('webview'); this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Date.now()}`); - // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick) - this._webview.setAttribute('disableblinkfeatures', 'Auxclick'); - this._webview.setAttribute('disableguestresize', ''); this._webview.setAttribute('webpreferences', 'contextIsolation=yes'); @@ -65,7 +58,7 @@ export class WebviewElement { this._webview.src = this._options.useSameOriginForRoot ? require.toUrl('./webview.html') : 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E'; this._ready = new Promise(resolve => { - const subscription = addDisposableListener(this._webview, 'ipc-message', (event) => { + const subscription = this._register(addDisposableListener(this._webview, 'ipc-message', (event) => { if (event.channel === 'webview-ready') { // console.info('[PID Webview] ' event.args[0]); addClass(this._webview, 'ready'); // can be found by debug command @@ -73,12 +66,12 @@ export class WebviewElement { subscription.dispose(); resolve(this); } - }); + })); }); if (!this._options.useSameOriginForRoot) { let loaded = false; - this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => { + this._register(addDisposableListener(this._webview, 'did-start-loading', () => { if (loaded) { return; } @@ -91,7 +84,7 @@ export class WebviewElement { if (!this._options.allowSvgs) { let loaded = false; - this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => { + this._register(addDisposableListener(this._webview, 'did-start-loading', () => { if (loaded) { return; } @@ -127,72 +120,59 @@ export class WebviewElement { })); } - this._disposables.push( - addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) { - console.log(`[Embedded Page] ${e.message}`); - }), - addDisposableListener(this._webview, 'dom-ready', () => { - this.layout(); - }), - addDisposableListener(this._webview, 'crashed', () => { - console.error('embedded page crashed'); - }), - addDisposableListener(this._webview, 'ipc-message', (event) => { - switch (event.channel) { - case 'onmessage': - if (this._options.enableWrappedPostMessage && event.args && event.args.length) { - this._onMessage.fire(event.args[0]); - } - return; + this._register(addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) { + console.log(`[Embedded Page] ${e.message}`); + })); + this._register(addDisposableListener(this._webview, 'dom-ready', () => { + this.layout(); + })); + this._register(addDisposableListener(this._webview, 'crashed', () => { + console.error('embedded page crashed'); + })); + this._register(addDisposableListener(this._webview, 'ipc-message', (event) => { + switch (event.channel) { + case 'onmessage': + if (this._options.enableWrappedPostMessage && event.args && event.args.length) { + this._onMessage.fire(event.args[0]); + } + return; - case 'did-click-link': - let [uri] = event.args; - this._onDidClickLink.fire(URI.parse(uri)); - return; + case 'did-click-link': + let [uri] = event.args; + this._onDidClickLink.fire(URI.parse(uri)); + return; - case 'did-set-content': - this._webview.style.flex = ''; - this._webview.style.width = '100%'; - this._webview.style.height = '100%'; - this.layout(); - return; + case 'did-set-content': + this._webview.style.flex = ''; + this._webview.style.width = '100%'; + this._webview.style.height = '100%'; + this.layout(); + return; - case 'did-scroll': - if (event.args && typeof event.args[0] === 'number') { - this._onDidScroll.fire({ scrollYPercentage: event.args[0] }); - } - return; + case 'did-scroll': + if (event.args && typeof event.args[0] === 'number') { + this._onDidScroll.fire({ scrollYPercentage: event.args[0] }); + } + return; - case 'do-reload': - this.reload(); - return; + case 'do-reload': + this.reload(); + return; - case 'do-update-state': - this._state = event.args[0]; - this._onDidUpdateState.fire(this._state); - return; - } - }), - addDisposableListener(this._webview, 'focus', () => { - if (this._contextKey) { - this._contextKey.set(true); - } - }), - addDisposableListener(this._webview, 'blur', () => { - if (this._contextKey) { - this._contextKey.reset(); - } - }), - addDisposableListener(this._webview, 'devtools-opened', () => { - this._send('devtools-opened'); - }), - ); + case 'do-update-state': + this._state = event.args[0]; + this._onDidUpdateState.fire(this._state); + return; + } + })); + this._register(addDisposableListener(this._webview, 'devtools-opened', () => { + this._send('devtools-opened'); + })); - this._webviewFindWidget = this._instantiationService.createInstance(WebviewFindWidget, this); - this._disposables.push(this._webviewFindWidget); + this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.style(this._themeService.getTheme()); - this._themeService.onThemeChange(this.style, this, this._disposables); + this._register(this._themeService.onThemeChange(this.style, this)); } public mountTo(parent: HTMLElement) { @@ -200,40 +180,31 @@ export class WebviewElement { parent.appendChild(this._webview); } - public notifyFindWidgetFocusChanged(isFocused: boolean) { - this._contextKey.set(isFocused || document.activeElement === this._webview); - } - - public notifyFindWidgetInputFocusChanged(isFocused: boolean) { - this._findInputContextKey.set(isFocused); - } - dispose(): void { - this._onDidClickLink.dispose(); - this._disposables = dispose(this._disposables); + if (this._webview) { + this._webview.guestinstance = 'none'; - if (this._contextKey) { - this._contextKey.reset(); + if (this._webview.parentElement) { + this._webview.parentElement.removeChild(this._webview); + } } - if (this._webview.parentElement) { - this._webview.parentElement.removeChild(this._webview); - } - - this._webviewFindWidget.dispose(); + this._webview = undefined; + this._webviewFindWidget = undefined; + super.dispose(); } - private readonly _onDidClickLink = new Emitter(); - public readonly onDidClickLink: Event = this._onDidClickLink.event; + private readonly _onDidClickLink = this._register(new Emitter()); + public readonly onDidClickLink = this._onDidClickLink.event; - private readonly _onDidScroll = new Emitter<{ scrollYPercentage: number }>(); - public readonly onDidScroll: Event<{ scrollYPercentage: number }> = this._onDidScroll.event; + private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number }>()); + public readonly onDidScroll = this._onDidScroll.event; - private readonly _onDidUpdateState = new Emitter(); - public readonly onDidUpdateState: Event = this._onDidUpdateState.event; + private readonly _onDidUpdateState = this._register(new Emitter()); + public readonly onDidUpdateState = this._onDidUpdateState.event; - private readonly _onMessage = new Emitter(); - public readonly onMessage: Event = this._onMessage.event; + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage = this._onMessage.event; private _send(channel: string, ...args: any[]): void { this._ready @@ -250,7 +221,16 @@ export class WebviewElement { } public set options(value: WebviewOptions) { + if (this._options && areWebviewInputOptionsEqual(value, this._options)) { + return; + } + this._options = value; + this._send('content', { + contents: this._contents, + options: this._options, + state: this._state + }); } public set contents(value: string) { @@ -262,6 +242,16 @@ export class WebviewElement { }); } + public update(value: string, options: WebviewOptions) { + this._contents = value; + this._options = options; + this._send('content', { + contents: this._contents, + options: this._options, + state: this._state + }); + } + public set baseUrl(value: string) { this._send('baseUrl', value); } @@ -363,11 +353,11 @@ export class WebviewElement { const appRootUri = URI.file(this._environmentService.appRoot); - registerFileProtocol(contents, 'vscode-core-resource', this._fileService, () => [ + registerFileProtocol(contents, WebviewProtocol.CoreResource, this._fileService, () => [ appRootUri ]); - registerFileProtocol(contents, 'vscode-resource', this._fileService, () => + registerFileProtocol(contents, WebviewProtocol.VsCodeResource, this._fileService, () => (this._options.localResourceRoots || []) ); } @@ -427,17 +417,13 @@ export class WebviewElement { this._webviewFindWidget.hide(); } - public showNextFindTerm() { - this._webviewFindWidget.showNextFindTerm(); - } - - public showPreviousFindTerm() { - this._webviewFindWidget.showPreviousFindTerm(); - } - public reload() { this.contents = this._contents; } + + public selectAll() { + this._webview.selectAll(); + } } @@ -458,44 +444,3 @@ namespace ApiThemeClassName { } } } - -function registerFileProtocol( - contents: Electron.WebContents, - protocol: string, - fileService: IFileService, - getRoots: () => ReadonlyArray -) { - contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => { - const requestPath = URI.parse(request.url).path; - const normalizedPath = URI.file(requestPath); - for (const root of getRoots()) { - if (startsWith(normalizedPath.fsPath, root.fsPath + nativeSep)) { - fileService.resolveContent(normalizedPath, { encoding: 'binary' }).then(contents => { - const mime = getMimeType(normalizedPath); - callback({ - data: Buffer.from(contents.value, contents.encoding), - mimeType: mime - }); - }, () => { - callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); - }); - return; - } - } - console.error('Webview: Cannot load resource outside of protocol root'); - callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); - }, (error) => { - if (error) { - console.error('Failed to register protocol ' + protocol); - } - }); -} - -const webviewMimeTypes = { - '.svg': 'image/svg+xml' -}; - -function getMimeType(normalizedPath: URI) { - const ext = extname(normalizedPath.fsPath).toLowerCase(); - return webviewMimeTypes[ext] || getMediaMime(normalizedPath.fsPath) || guessMimeTypes(normalizedPath.fsPath)[0]; -} diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts b/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts index 53ac36126e8..3c881b1ba09 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewFindWidget.ts @@ -7,58 +7,49 @@ import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { WebviewElement } from './webviewElement'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; export class WebviewFindWidget extends SimpleFindWidget { constructor( - private readonly webview: WebviewElement, + private _webview: WebviewElement, @IContextViewService contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService notificationService: INotificationService, - @IStorageService storageService: IStorageService + @IContextKeyService contextKeyService: IContextKeyService ) { - super(contextViewService, contextKeyService, keybindingService, notificationService, storageService); + super(contextViewService, contextKeyService); + } + + dispose() { + this._webview = undefined; + super.dispose(); } public find(previous: boolean) { const val = this.inputValue; if (val) { - this.webview.find(val, { findNext: true, forward: !previous }); + this._webview.find(val, { findNext: true, forward: !previous }); } } public hide() { super.hide(); - this.webview.stopFind(true); - this.webview.focus(); + this._webview.stopFind(true); + this._webview.focus(); } public onInputChanged() { const val = this.inputValue; if (val) { - this.webview.startFind(val); + this._webview.startFind(val); } else { - this.webview.stopFind(false); + this._webview.stopFind(false); } } - protected onFocusTrackerFocus() { - this.webview.notifyFindWidgetFocusChanged(true); - } + protected onFocusTrackerFocus() { } - protected onFocusTrackerBlur() { - this.webview.notifyFindWidgetFocusChanged(false); - } + protected onFocusTrackerBlur() { } - protected onFindInputFocusTrackerFocus() { - this.webview.notifyFindWidgetInputFocusChanged(true); - } + protected onFindInputFocusTrackerFocus() { } - protected onFindInputFocusTrackerBlur() { - this.webview.notifyFindWidgetInputFocusChanged(false); - } + protected onFindInputFocusTrackerBlur() { } } \ No newline at end of file diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewProtocols.ts b/src/vs/workbench/parts/webview/electron-browser/webviewProtocols.ts new file mode 100644 index 00000000000..5b2fadb82f9 --- /dev/null +++ b/src/vs/workbench/parts/webview/electron-browser/webviewProtocols.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { extname } from 'path'; +import { getMediaMime, guessMimeTypes } from 'vs/base/common/mime'; +import { nativeSep } from 'vs/base/common/paths'; +import { startsWith } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; + +export const enum WebviewProtocol { + CoreResource = 'vscode-core-resource', + VsCodeResource = 'vscode-resource' +} + +export function registerFileProtocol( + contents: Electron.WebContents, + protocol: WebviewProtocol, + fileService: IFileService, + getRoots: () => ReadonlyArray +) { + contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => { + const requestPath = URI.parse(request.url).path; + const normalizedPath = URI.file(requestPath); + for (const root of getRoots()) { + if (startsWith(normalizedPath.fsPath, root.fsPath + nativeSep)) { + fileService.resolveContent(normalizedPath, { encoding: 'binary' }).then(contents => { + const mime = getMimeType(normalizedPath); + callback({ + data: Buffer.from(contents.value, contents.encoding), + mimeType: mime + }); + }, () => { + callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); + }); + return; + } + } + console.error('Webview: Cannot load resource outside of protocol root'); + callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); + }, (error) => { + if (error) { + console.error('Failed to register protocol ' + protocol); + } + }); +} + +const webviewMimeTypes = { + '.svg': 'image/svg+xml' +}; + +function getMimeType(normalizedPath: URI) { + const ext = extname(normalizedPath.fsPath).toLowerCase(); + return webviewMimeTypes[ext] || getMediaMime(normalizedPath.fsPath) || guessMimeTypes(normalizedPath.fsPath)[0]; +} diff --git a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts index 49a9d309bbf..40f1d056f1e 100644 --- a/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts +++ b/src/vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut.ts @@ -10,49 +10,136 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import product from 'vs/platform/node/product'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IExperimentService, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { language, locale } from 'vs/base/common/platform'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; export class TelemetryOptOut implements IWorkbenchContribution { private static TELEMETRY_OPT_OUT_SHOWN = 'workbench.telemetryOptOutShown'; + private privacyUrl: string; + private optOutUrl: string; constructor( @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, - @INotificationService notificationService: INotificationService, + @INotificationService private notificationService: INotificationService, @IWindowService windowService: IWindowService, @IWindowsService windowsService: IWindowsService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService private telemetryService: ITelemetryService, + @IExperimentService private experimentService: IExperimentService, + @IConfigurationService private configurationService: IConfigurationService, + @IExtensionGalleryService private galleryService: IExtensionGalleryService ) { if (!product.telemetryOptOutUrl || storageService.get(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN)) { return; } + const experimentId = 'telemetryOptOut'; Promise.all([ windowService.isFocused(), - windowsService.getWindowCount() - ]).then(([focused, count]) => { + windowsService.getWindowCount(), + experimentService.getExperimentById(experimentId) + ]).then(([focused, count, experimentState]) => { if (!focused && count > 1) { return null; } storageService.store(TelemetryOptOut.TELEMETRY_OPT_OUT_SHOWN, true); - const optOutUrl = product.telemetryOptOutUrl; - const privacyUrl = product.privacyStatementUrl || product.telemetryOptOutUrl; - const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt out]({1}).", privacyUrl, optOutUrl); - const optInNotice = localize('telemetryOptOut.optInNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt in]({1}).", privacyUrl, optOutUrl); + this.optOutUrl = product.telemetryOptOutUrl; + this.privacyUrl = product.privacyStatementUrl || product.telemetryOptOutUrl; + + if (experimentState && experimentState.state === ExperimentState.Run && telemetryService.isOptedIn) { + this.runExperiment(experimentId); + return; + } + + const optOutNotice = localize('telemetryOptOut.optOutNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt out]({1}).", this.privacyUrl, this.optOutUrl); + const optInNotice = localize('telemetryOptOut.optInNotice', "Help improve VS Code by allowing Microsoft to collect usage data. Read our [privacy statement]({0}) and learn how to [opt in]({1}).", this.privacyUrl, this.optOutUrl); notificationService.prompt( Severity.Info, telemetryService.isOptedIn ? optOutNotice : optInNotice, [{ label: localize('telemetryOptOut.readMore', "Read More"), - run: () => openerService.open(URI.parse(optOutUrl)) + run: () => openerService.open(URI.parse(this.optOutUrl)) }] ); }) .then(null, onUnexpectedError); } + + private runExperiment(experimentId: string) { + const promptMessageKey = 'telemetryOptOut.optOutOption'; + const yesLabelKey = 'telemetryOptOut.OptIn'; + const noLabelKey = 'telemetryOptOut.OptOut'; + + let promptMessage = localize('telemetryOptOut.optOutOption', "Please help Microsoft improve Visual Studio Code by allowing the collection of usage data. Read our [privacy statement]({0}) for more details.", this.privacyUrl); + let yesLabel = localize('telemetryOptOut.OptIn', "Yes, glad to help"); + let noLabel = localize('telemetryOptOut.OptOut', "No, thanks"); + + let queryPromise = TPromise.as(undefined); + if ((locale !== language && locale !== 'en' && locale.indexOf('en-') === -1)) { + queryPromise = this.galleryService.query({ text: `tag:lp-${locale}` }).then(tagResult => { + if (!tagResult || !tagResult.total) { + return undefined; + } + const extensionToFetchTranslationsFrom = tagResult.firstPage.filter(e => e.publisher === 'MS-CEINTL' && e.name.indexOf('vscode-language-pack') === 0)[0] || tagResult.firstPage[0]; + if (!extensionToFetchTranslationsFrom.assets || !extensionToFetchTranslationsFrom.assets.coreTranslations) { + return undefined; + } + + return this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, locale) + .then(translation => { + const translationsFromPack = translation && translation.contents ? translation.contents['vs/workbench/parts/welcome/gettingStarted/electron-browser/telemetryOptOut'] : {}; + if (!!translationsFromPack[promptMessageKey] && !!translationsFromPack[yesLabelKey] && !!translationsFromPack[noLabelKey]) { + promptMessage = translationsFromPack[promptMessageKey].replace('{0}', this.privacyUrl) + ' (Please help Microsoft improve Visual Studio Code by allowing the collection of usage data.)'; + yesLabel = translationsFromPack[yesLabelKey] + ' (Yes)'; + noLabel = translationsFromPack[noLabelKey] + ' (No)'; + } + return undefined; + }); + + }); + } + + const logTelemetry = (optout?: boolean) => { + /* __GDPR__ + "experiments:optout" : { + "optOut": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('experiments:optout', typeof optout === 'boolean' ? { optout } : {}); + }; + + queryPromise.then(() => { + this.notificationService.prompt( + Severity.Info, + promptMessage, + [ + { + label: yesLabel, + run: () => { + logTelemetry(false); + } + }, + { + label: noLabel, + run: () => { + logTelemetry(true); + this.configurationService.updateValue('telemetry.enableTelemetry', false); + this.configurationService.updateValue('telemetry.enableCrashReporter', false); + } + } + ], + logTelemetry + ); + this.experimentService.markAsCompleted(experimentId); + }); + } } diff --git a/src/vs/workbench/parts/welcome/overlay/browser/media/commandpalette.svg b/src/vs/workbench/parts/welcome/overlay/browser/media/commandpalette.svg index 56e91eb8410..5d4d668d107 100644 --- a/src/vs/workbench/parts/welcome/overlay/browser/media/commandpalette.svg +++ b/src/vs/workbench/parts/welcome/overlay/browser/media/commandpalette.svg @@ -1 +1 @@ -Asset 9 \ No newline at end of file +Asset 9 \ No newline at end of file diff --git a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css index bc843e58ab3..e76b3e08093 100644 --- a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css +++ b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.css @@ -97,6 +97,12 @@ left: 45px; } +.monaco-workbench > .welcomeOverlay > .key.terminal { + position: absolute; + bottom: 25px; + left: 50%; +} + .monaco-workbench > .welcomeOverlay > .key.notifications { position: absolute; bottom: 25px; diff --git a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts index 8bff80b2110..3f3b889a802 100644 --- a/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts +++ b/src/vs/workbench/parts/welcome/overlay/browser/welcomeOverlay.ts @@ -5,7 +5,6 @@ 'use strict'; import 'vs/css!./welcomeOverlay'; -import { $, Builder } from 'vs/base/browser/builder'; import * as dom from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -26,9 +25,11 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { textPreformatForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; +const $ = dom.$; + interface Key { id: string; - arrow: string; + arrow?: string; label: string; command?: string; arrowLast?: boolean; @@ -78,6 +79,11 @@ const keys: Key[] = [ label: localize('welcomeOverlay.problems', "View errors and warnings"), command: 'workbench.actions.view.problems' }, + { + id: 'terminal', + label: localize('welcomeOverlay.terminal', "Toggle integrated terminal"), + command: 'workbench.action.terminal.toggleTerminal' + }, // { // id: 'openfile', // arrow: '⤸', @@ -150,7 +156,7 @@ class WelcomeOverlay { private _toDispose: IDisposable[] = []; private _overlayVisible: IContextKey; - private _overlay: Builder; + private _overlay: HTMLElement; constructor( @IPartService private partService: IPartService, @@ -167,40 +173,39 @@ class WelcomeOverlay { const container = this.partService.getContainer(Parts.EDITOR_PART); const offset = this.partService.getTitleBarOffset(); - this._overlay = $(container.parentElement) - .div({ 'class': 'welcomeOverlay' }) - .style({ top: `${offset}px` }) - .style({ height: `calc(100% - ${offset}px)` }) - .display('none'); + this._overlay = dom.append(container.parentElement, $('.welcomeOverlay')); + this._overlay.style.top = `${offset}px`; + this._overlay.style.height = `calc(100% - ${offset}px)`; + this._overlay.style.display = 'none'; - this._overlay.on('click', () => this.hide(), this._toDispose); + this._toDispose.push(dom.addStandardDisposableListener(this._overlay, 'click', () => this.hide())); this.commandService.onWillExecuteCommand(() => this.hide()); - $(this._overlay).div({ 'class': 'commandPalettePlaceholder' }); + dom.append(this._overlay, $('.commandPalettePlaceholder')); const editorOpen = !!this.editorService.visibleEditors.length; keys.filter(key => !('withEditor' in key) || key.withEditor === editorOpen) .forEach(({ id, arrow, label, command, arrowLast }) => { - const div = $(this._overlay).div({ 'class': ['key', id] }); - if (!arrowLast) { - $(div).span({ 'class': 'arrow' }).innerHtml(arrow); + const div = dom.append(this._overlay, $(`.key.${id}`)); + if (arrow && !arrowLast) { + dom.append(div, $('span.arrow')).innerHTML = arrow; } - $(div).span({ 'class': 'label' }).text(label); + dom.append(div, $('span.label')).textContent = label; if (command) { const shortcut = this.keybindingService.lookupKeybinding(command); if (shortcut) { - $(div).span({ 'class': 'shortcut' }).text(shortcut.getLabel()); + dom.append(div, $('span.shortcut')).textContent = shortcut.getLabel(); } } - if (arrowLast) { - $(div).span({ 'class': 'arrow' }).innerHtml(arrow); + if (arrow && arrowLast) { + dom.append(div, $('span.arrow')).innerHTML = arrow; } }); } public show() { - if (this._overlay.style('display') !== 'block') { - this._overlay.display('block'); + if (this._overlay.style.display !== 'block') { + this._overlay.style.display = 'block'; const workbench = document.querySelector('.monaco-workbench') as HTMLElement; dom.addClass(workbench, 'blur-background'); this._overlayVisible.set(true); @@ -210,10 +215,10 @@ class WelcomeOverlay { private updateProblemsKey() { const problems = document.querySelector('.task-statusbar-item'); - const key = this._overlay.getHTMLElement().querySelector('.key.problems') as HTMLElement; + const key = this._overlay.querySelector('.key.problems') as HTMLElement; if (problems instanceof HTMLElement) { const target = problems.getBoundingClientRect(); - const bounds = this._overlay.getHTMLElement().getBoundingClientRect(); + const bounds = this._overlay.getBoundingClientRect(); const bottom = bounds.bottom - target.top + 3; const left = (target.left + target.right) / 2 - bounds.left; key.style.bottom = bottom + 'px'; @@ -225,8 +230,8 @@ class WelcomeOverlay { } public hide() { - if (this._overlay.style('display') !== 'none') { - this._overlay.display('none'); + if (this._overlay.style.display !== 'none') { + this._overlay.style.display = 'none'; const workbench = document.querySelector('.monaco-workbench') as HTMLElement; dom.removeClass(workbench, 'blur-background'); this._overlayVisible.reset(); diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts b/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts index 27e73025e43..56ffc97bcb6 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page.ts @@ -55,11 +55,11 @@ export default () => `

diff --git a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts index 2e5f8844e43..99341e7cabe 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.contribution.ts @@ -9,7 +9,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { Registry } from 'vs/platform/registry/common/platform'; import { WelcomePageContribution, WelcomePageAction, WelcomeInputFactory } from 'vs/workbench/parts/welcome/page/electron-browser/welcomePage'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -26,10 +26,10 @@ Registry.as(ConfigurationExtensions.Configuration) 'enumDescriptions': [ localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page (default)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty workspace)."), ], 'default': 'welcomePage', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none is restored from the previous session. Select 'none' to start without an editor, 'welcomePage' to open the Welcome page (default), 'newUntitledFile' to open a new untitled file (only opening an empty workspace).") + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") }, } }); @@ -41,3 +41,12 @@ Registry.as(ActionExtensions.WorkbenchActions) .registerWorkbenchAction(new SyncActionDescriptor(WelcomePageAction, WelcomePageAction.ID, WelcomePageAction.LABEL), 'Help: Welcome', localize('help', "Help")); Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(WelcomeInputFactory.ID, WelcomeInputFactory); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.showWelcomePage', + title: localize({ key: 'miWelcome', comment: ['&& denotes a mnemonic'] }, "&&Welcome") + }, + order: 1 +}); 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 67102680756..f078908a91c 100644 --- a/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts +++ b/src/vs/workbench/parts/welcome/page/electron-browser/welcomePage.ts @@ -5,7 +5,7 @@ 'use strict'; import 'vs/css!./welcomePage'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as path from 'path'; import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughInput'; @@ -24,7 +24,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Schemas } from 'vs/base/common/network'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { getInstalledExtensions, IExtensionStatus, onExtensionChanged, isKeymapExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils'; -import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, EnablementState, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { used } from 'vs/workbench/parts/welcome/page/electron-browser/vs_code_welcome_page'; import { ILifecycleService, StartupKind } from 'vs/platform/lifecycle/common/lifecycle'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -33,11 +33,13 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughUtils'; import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ILabelService } from 'vs/platform/label/common/label'; used(); @@ -52,9 +54,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { @IConfigurationService configurationService: IConfigurationService, @IEditorService editorService: IEditorService, @IBackupFileService backupFileService: IBackupFileService, - @ITelemetryService telemetryService: ITelemetryService, @ILifecycleService lifecycleService: ILifecycleService, - @IStorageService storageService: IStorageService ) { const enabled = isWelcomePageEnabled(configurationService); if (enabled && lifecycleService.startupKind !== StartupKind.ReloadedWindow) { @@ -223,6 +223,7 @@ class WelcomePage { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService, @IEnvironmentService private environmentService: IEnvironmentService, + @ILabelService private labelService: ILabelService, @INotificationService private notificationService: INotificationService, @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, @IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService, @@ -254,7 +255,7 @@ class WelcomePage { return this.editorService.openEditor(this.editorInput, { pinned: false }); } - private onReady(container: HTMLElement, recentlyOpened: TPromise<{ files: string[]; workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; }>, installedExtensions: TPromise): void { + private onReady(container: HTMLElement, recentlyOpened: TPromise<{ files: URI[]; workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]; }>, installedExtensions: TPromise): void { const enabled = isWelcomePageEnabled(this.configurationService); const showOnStartup = container.querySelector('#showOnStartup'); if (enabled) { @@ -276,33 +277,40 @@ class WelcomePage { const before = ul.firstElementChild; workspaces.slice(0, 5).forEach(workspace => { let label: string; - let parent: string; - let wsPath: string; + let resource: URI; if (isSingleFolderWorkspaceIdentifier(workspace)) { - label = getBaseLabel(workspace); - parent = path.dirname(workspace); - wsPath = workspace; + resource = workspace; + label = this.labelService.getWorkspaceLabel(workspace); + } else if (isWorkspaceIdentifier(workspace)) { + label = this.labelService.getWorkspaceLabel(workspace); + resource = URI.file(workspace.configPath); } else { - label = getWorkspaceLabel(workspace, this.environmentService); - parent = path.dirname(workspace.configPath); - wsPath = workspace.configPath; + label = getBaseLabel(workspace); + resource = URI.file(workspace); } const li = document.createElement('li'); const a = document.createElement('a'); let name = label; - let parentFolder = parent; - if (!name && parentFolder) { - const tmp = name; - name = parentFolder; - parentFolder = tmp; + let parentFolderPath: string; + + if (resource.scheme === Schemas.file) { + let parentFolder = path.dirname(resource.fsPath); + if (!name && parentFolder) { + const tmp = name; + name = parentFolder; + parentFolder = tmp; + } + parentFolderPath = tildify(parentFolder, this.environmentService.userHome); + } else { + parentFolderPath = this.labelService.getUriLabel(resource); } - const tildifiedParentFolder = tildify(parentFolder, this.environmentService.userHome); + a.innerText = name; a.title = label; - a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, tildifiedParentFolder)); + a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentFolderPath)); a.href = 'javascript:void(0)'; a.addEventListener('click', e => { /* __GDPR__ @@ -315,7 +323,7 @@ class WelcomePage { id: 'openRecentFolder', from: telemetryFrom }); - this.windowService.openWindow([wsPath], { forceNewWindow: e.ctrlKey || e.metaKey }); + this.windowService.openWindow([resource], { forceNewWindow: e.ctrlKey || e.metaKey }); e.preventDefault(); e.stopPropagation(); }); @@ -324,7 +332,7 @@ class WelcomePage { const span = document.createElement('span'); span.classList.add('path'); span.classList.add('detail'); - span.innerText = tildifiedParentFolder; + span.innerText = parentFolderPath; span.title = label; li.appendChild(span); @@ -419,7 +427,9 @@ class WelcomePage { return null; } return this.extensionManagementService.installFromGallery(extension) - .then(local => { + .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) + .then(installed => { + const local = installed.filter(i => areSameExtensions(extension.identifier, i.galleryIdentifier))[0]; // TODO: Do this as part of the install to avoid multiple events. return this.extensionEnablementService.setEnablement(local, EnablementState.Disabled).then(() => local); }); @@ -431,10 +441,10 @@ class WelcomePage { [{ label: localize('ok', "OK"), run: () => { - const messageDelay = TPromise.timeout(300); - messageDelay.then(() => { + const messageDelay = new TimeoutTimer(); + messageDelay.cancelAndSet(() => { this.notificationService.info(strings.installing.replace('{0}', extensionSuggestion.name)); - }); + }, 300); TPromise.join(extensionSuggestion.isKeymap ? extensions.filter(extension => isKeymapExtension(this.tipsService, extension) && extension.globallyEnabled) .map(extension => { return this.extensionEnablementService.setEnablement(extension.local, EnablementState.Disabled); diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/editorWalkThrough.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/editorWalkThrough.ts index 71316038c95..5599f8459e1 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/editorWalkThrough.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/editorWalkThrough.ts @@ -9,7 +9,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Action } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { WalkThroughInput, WalkThroughInputOptions } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughInput'; import { Schemas } from 'vs/base/common/network'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md index 9f5fcd08827..43e8bc83a89 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/editor/vs_code_editor_walkthrough.md @@ -17,16 +17,16 @@ The core editor in VS Code is packed with features. This page highlights a numb ### Multi-Cursor Editing Using multiple cursors allows you to edit multiple parts of the document at once, greatly improving your productivity. Try the following actions in the code block below: -1. Box Selection - press any combination of kb(cursorColumnSelectDown), kb(cursorColumnSelectRight), kb(cursorColumnSelectUp), kb(cursorColumnSelectLeft) to select a block of text. You can also press `⇧⌥``Shift+Alt` while selecting text with the mouse. +1. Box Selection - press any combination of kb(cursorColumnSelectDown), kb(cursorColumnSelectRight), kb(cursorColumnSelectUp), kb(cursorColumnSelectLeft) to select a block of text. You can also press `⇧⌥``Shift+Alt` while selecting text with the mouse or drag-select using the middle mouse button. 2. Add a cursor - press kb(editor.action.insertCursorAbove) to add a new cursor above, or kb(editor.action.insertCursorBelow) to add a new cursor below. You can also use your mouse with +Click to add a cursor anywhere. 3. Create cursors on all occurrences of a string - select one instance of a string e.g. `background-color` and press kb(editor.action.selectHighlights). Now you can replace all instances by simply typing. That is the tip of the iceberg for multi-cursor editing. Have a look at the selection menu and our handy [keyboard reference guide](command:workbench.action.keybindingsReference) for additional actions. ```css -#p1 {background-color: #ff0000;} /* red */ -#p2 {background-color: #00ff00;} /* green */ -#p3 {background-color: #0000ff;} /* blue */ +#p1 {background-color: #ff0000;} /* red in HEX format */ +#p2 {background-color: hsl(120, 100%, 50%);} /* green in HSL format */ +#p3 {background-color: rgba(0, 4, 255, 0.733);} /* blue with alpha channel in RGBA format */ ``` > **CSS Tip:** you may have noticed in the example above we also provide color swatches inline for CSS, additionally if you hover over an element such as `#p1` we will show how this is represented in HTML. These swatches also act as color pickers that allow you to easily change a color value. A simple example of some language-specific editor features. @@ -36,10 +36,10 @@ That is the tip of the iceberg for multi-cursor editing. Have a look at the sele Visual Studio Code comes with the powerful IntelliSense for JavaScript and TypeScript pre-installed. In the below example, position the text cursor in front of the error underline, right after the dot and press kb(editor.action.triggerSuggest) to invoke IntelliSense. Notice how the suggestion comes from the Request API. ```js -var express = require('express'); -var app = express(); +const express = require('express'); +const app = express(); -app.get('/', function (req, res) { +app.get('/', (req, res) => { res.send(`Hello ${req.}`); }); @@ -92,9 +92,9 @@ Sometimes you want to refactor already written code into a separate function or ```js function findFirstEvenNumber(arr) { - for (let i = 0; i < arr.length; i++) { - if (typeof arr[i] === 'number' && arr[i] % 2 === 0) { - return arr[i]; + for (const el of arr) { + if (typeof el === 'number' && el % 2 === 0) { + return el; } } return null; @@ -103,15 +103,15 @@ function findFirstEvenNumber(arr) { ### Formatting -Keeping your code looking great is hard without a good formatter. Luckily it's easy to format content either the entire document with kb(editor.action.formatDocument). Formatting can be applied to the current selection with kb(editor.action.formatSelection). Both of these options are also available through the right-click context menu. +Keeping your code looking great is hard without a good formatter. Luckily it's easy to format content, either for the entire document with kb(editor.action.formatDocument) or for the current selection with kb(editor.action.formatSelection). Both of these options are also available through the right-click context menu. ```js -var cars = ["Saab", "Volvo", "BMW"]; +const cars = ["🚗", "🚙", "🚕"]; -for (var i=0; i < cars.length; i++) { -// Drive the car -console.log(`This is the manufacturer [${cars[i]}])`); - } +for (const car of cars){ + // Drive the car + console.log(`This is the car ${car}`); +} ``` >**Tip:** Additional formatters are available in the [extension gallery](command:workbench.extensions.action.showPopularExtensions). Formatting support can also be configured via [settings](command:workbench.action.openGlobalSettings) e.g. enabling `editor.formatOnSave`. @@ -137,7 +137,7 @@ In a large file it can often be useful to collapse sections of code to increase >**Tip:** Folding is based on indentation and as a result can apply to all languages. Simply indent your code to create a foldable section you can fold a certain number of levels with shortcuts like kb(editor.foldLevel1) through to kb(editor.foldLevel5). ### Errors and Warnings -Errors and warnings are highlighted as you edit your code with squiggles. In the sample below you can see a number of syntax errors. By pressing kb(editor.action.marker.next) you can navigate across them in sequence and see the detailed error message. As you correct them the squiggles and scrollbar indicators will update. +Errors and warnings are highlighted as you edit your code with squiggles. In the sample below you can see a number of syntax errors. By pressing kb(editor.action.marker.nextInFiles) you can navigate across them in sequence and see the detailed error message. As you correct them the squiggles and scrollbar indicators will update. ```js // This code has a few syntax errors @@ -192,4 +192,4 @@ Well if you have got this far then you will have touched on some of the editing That's all for now, -Happy Coding! \ No newline at end of file +Happy Coding! 🎉 diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution.ts index 335ba65618e..f0397cfb446 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThrough.contribution.ts @@ -14,7 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IEditorRegistry, Extensions as EditorExtensions, EditorDescriptor } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -48,3 +48,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule(WalkThroughArrowDown); KeybindingsRegistry.registerCommandAndKeybindingRule(WalkThroughPageUp); KeybindingsRegistry.registerCommandAndKeybindingRule(WalkThroughPageDown); + +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.showInteractivePlayground', + title: localize({ key: 'miInteractivePlayground', comment: ['&& denotes a mnemonic'] }, "&&Interactive Playground") + }, + order: 2 +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughActions.ts b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughActions.ts index 5a838046aa8..6fc211d61f6 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughActions.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughActions.ts @@ -6,14 +6,14 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WalkThroughPart, WALK_THROUGH_FOCUS } from 'vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart'; -import { ICommandAndKeybindingRule, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICommandAndKeybindingRule, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeyCode } from 'vs/base/common/keyCodes'; export const WalkThroughArrowUp: ICommandAndKeybindingRule = { id: 'workbench.action.interactivePlayground.arrowUp', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(WALK_THROUGH_FOCUS, EditorContextKeys.editorTextFocus.toNegated()), primary: KeyCode.UpArrow, handler: accessor => { @@ -27,7 +27,7 @@ export const WalkThroughArrowUp: ICommandAndKeybindingRule = { export const WalkThroughArrowDown: ICommandAndKeybindingRule = { id: 'workbench.action.interactivePlayground.arrowDown', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(WALK_THROUGH_FOCUS, EditorContextKeys.editorTextFocus.toNegated()), primary: KeyCode.DownArrow, handler: accessor => { @@ -41,7 +41,7 @@ export const WalkThroughArrowDown: ICommandAndKeybindingRule = { export const WalkThroughPageUp: ICommandAndKeybindingRule = { id: 'workbench.action.interactivePlayground.pageUp', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(WALK_THROUGH_FOCUS, EditorContextKeys.editorTextFocus.toNegated()), primary: KeyCode.PageUp, handler: accessor => { @@ -55,7 +55,7 @@ export const WalkThroughPageUp: ICommandAndKeybindingRule = { export const WalkThroughPageDown: ICommandAndKeybindingRule = { id: 'workbench.action.interactivePlayground.pageDown', - weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(WALK_THROUGH_FOCUS, EditorContextKeys.editorTextFocus.toNegated()), primary: KeyCode.PageDown, handler: accessor => { 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 d6438940e37..54372b51861 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart.ts @@ -9,9 +9,9 @@ import 'vs/css!./walkThroughPart'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; -import URI from 'vs/base/common/uri'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { EditorOptions, EditorViewStateMemento } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/parts/welcome/walkThrough/node/walkThroughInput'; @@ -23,7 +23,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { localize } from 'vs/nls'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { Scope } from 'vs/workbench/common/memento'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { once } from 'vs/base/common/event'; @@ -65,7 +64,7 @@ export class WalkThroughPart extends BaseEditor { private scrollbar: DomScrollableElement; private editorFocus: IContextKey; private size: Dimension; - private editorViewStateMemento: EditorViewStateMemento; + private editorMemento: IEditorMemento; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -82,7 +81,7 @@ export class WalkThroughPart extends BaseEditor { ) { super(WalkThroughPart.ID, telemetryService, themeService); this.editorFocus = WALK_THROUGH_FOCUS.bindTo(this.contextKeyService); - this.editorViewStateMemento = new EditorViewStateMemento(editorGroupService, this.getMemento(storageService, Scope.WORKSPACE), WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); + this.editorMemento = this.getEditorMemento(storageService, editorGroupService, WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); } createEditor(container: HTMLElement): void { @@ -118,7 +117,7 @@ export class WalkThroughPart extends BaseEditor { private addEventListener(element: E, type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): IDisposable; private addEventListener(element: E, type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): IDisposable { element.addEventListener(type, listener, useCapture); - return { dispose: () => { element.removeEventListener(type, listener, useCapture); } }; + return toDisposable(() => { element.removeEventListener(type, listener, useCapture); }); } private registerFocusHandlers() { @@ -260,7 +259,7 @@ export class WalkThroughPart extends BaseEditor { return super.setInput(input, options, token) .then(() => { - return input.resolve(true); + return input.resolve(); }) .then(model => { if (token.isCancellationRequested) { @@ -476,7 +475,7 @@ export class WalkThroughPart extends BaseEditor { private saveTextEditorViewState(input: WalkThroughInput): void { const scrollPosition = this.scrollbar.getScrollPosition(); - this.editorViewStateMemento.saveState(this.group, input, { + this.editorMemento.saveState(this.group, input, { viewState: { scrollTop: scrollPosition.scrollTop, scrollLeft: scrollPosition.scrollLeft @@ -485,7 +484,7 @@ export class WalkThroughPart extends BaseEditor { } private loadTextEditorViewState(input: WalkThroughInput) { - const state = this.editorViewStateMemento.loadState(this.group, input); + const state = this.editorMemento.loadState(this.group, input); if (state) { this.scrollbar.setScrollPosition(state.viewState); } @@ -505,14 +504,6 @@ export class WalkThroughPart extends BaseEditor { super.shutdown(); } - protected saveMemento(): void { - - // ensure to first save our view state memento - this.editorViewStateMemento.save(); - - super.saveMemento(); - } - dispose(): void { this.editorFocus.reset(); this.contentDisposables = dispose(this.contentDisposables); @@ -526,7 +517,7 @@ export class WalkThroughPart extends BaseEditor { export const embeddedEditorBackground = registerColor('walkThrough.embeddedEditorBackground', { dark: null, light: null, hc: null }, localize('walkThrough.embeddedEditorBackground', 'Background color for the embedded editors on the Interactive Playground.')); registerThemingParticipant((theme, collector) => { - const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: 'rgba(0,0,0,.08)', hc: null }); + 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}; }`); diff --git a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts index ea02659a15a..04ed9af895e 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts @@ -6,7 +6,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts index 81b269fa5e6..12027989976 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts +++ b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughInput.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput, EditorModel, ITextEditorModel } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IReference, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -102,7 +102,7 @@ export class WalkThroughInput extends EditorInput { return this.options.onReady; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { if (!this.promise) { this.promise = this.textModelResolverService.createModelReference(this.options.resource) .then(ref => { diff --git a/src/vs/workbench/services/actions/electron-browser/menusExtensionPoint.ts b/src/vs/workbench/services/actions/electron-browser/menusExtensionPoint.ts index ee1edcccdb9..df677f6fb3f 100644 --- a/src/vs/workbench/services/actions/electron-browser/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/electron-browser/menusExtensionPoint.ts @@ -6,12 +6,13 @@ import { localize } from 'vs/nls'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; -import { join } from 'path'; +import * as resources from 'vs/base/common/resources'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { forEach } from 'vs/base/common/collections'; import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; +import { URI } from 'vs/base/common/uri'; namespace schema { @@ -286,20 +287,19 @@ ExtensionsRegistry.registerExtensionPoint p.id === compositeOrActionId).length) { return this.panelPart.showActivity(compositeOrActionId, badge, clazz); } diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 6167a730512..27513b65417 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -13,19 +13,19 @@ export interface IBadge { } export class BaseBadge implements IBadge { - public descriptorFn: (args: any) => string; + descriptorFn: (args: any) => string; constructor(descriptorFn: (args: any) => string) { this.descriptorFn = descriptorFn; } - public getDescription(): string { + getDescription(): string { return this.descriptorFn(null); } } export class NumberBadge extends BaseBadge { - public number: number; + number: number; constructor(number: number, descriptorFn: (args: any) => string) { super(descriptorFn); @@ -33,13 +33,13 @@ export class NumberBadge extends BaseBadge { this.number = number; } - public getDescription(): string { + getDescription(): string { return this.descriptorFn(this.number); } } export class TextBadge extends BaseBadge { - public text: string; + text: string; constructor(text: string, descriptorFn: (args: any) => string) { super(descriptorFn); diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 86d5326107e..9c56b9d9fe1 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -5,7 +5,7 @@ 'use strict'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; @@ -22,11 +22,6 @@ export const BACKUP_FILE_UPDATE_OPTIONS: IUpdateContentOptions = { encoding: 'ut export interface IBackupFileService { _serviceBrand: any; - /** - * If backups are enabled. - */ - backupEnabled: boolean; - /** * Finds out if there are any backups stored. */ diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index fdeea689752..cd66beb4d76 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -8,14 +8,15 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as pfs from 'vs/base/node/pfs'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { ResourceQueue } from 'vs/base/common/async'; import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; import { IFileService, ITextSnapshot } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { readToMatchingString } from 'vs/base/node/stream'; import { ITextBufferFactory } from 'vs/editor/common/model'; -import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { keys } from 'vs/base/common/map'; export interface IBackupFilesModel { resolve(backupRoot: string): TPromise; @@ -31,10 +32,9 @@ export interface IBackupFilesModel { export class BackupSnapshot implements ITextSnapshot { private preambleHandled: boolean; - constructor(private snapshot: ITextSnapshot, private preamble: string) { - } + constructor(private snapshot: ITextSnapshot, private preamble: string) { } - public read(): string { + read(): string { let value = this.snapshot.read(); if (!this.preambleHandled) { this.preambleHandled = true; @@ -53,7 +53,7 @@ export class BackupSnapshot implements ITextSnapshot { export class BackupFilesModel implements IBackupFilesModel { private cache: { [resource: string]: number /* version ID */ } = Object.create(null); - public resolve(backupRoot: string): TPromise { + resolve(backupRoot: string): TPromise { return pfs.readDirsInDir(backupRoot).then(backupSchemas => { // For all supported schemas @@ -73,15 +73,15 @@ export class BackupFilesModel implements IBackupFilesModel { }).then(() => this, error => this); } - public add(resource: Uri, versionId = 0): void { + add(resource: Uri, versionId = 0): void { this.cache[resource.toString()] = versionId; } - public count(): number { + count(): number { return Object.keys(this.cache).length; } - public has(resource: Uri, versionId?: number): boolean { + has(resource: Uri, versionId?: number): boolean { const cachedVersionId = this.cache[resource.toString()]; if (typeof cachedVersionId !== 'number') { return false; // unknown resource @@ -94,15 +94,15 @@ export class BackupFilesModel implements IBackupFilesModel { return true; } - public get(): Uri[] { + get(): Uri[] { return Object.keys(this.cache).map(k => Uri.parse(k)); } - public remove(resource: Uri): void { + remove(resource: Uri): void { delete this.cache[resource.toString()]; } - public clear(): void { + clear(): void { this.cache = Object.create(null); } } @@ -111,7 +111,7 @@ export class BackupFileService implements IBackupFileService { private static readonly META_MARKER = '\n'; - public _serviceBrand: any; + _serviceBrand: any; private backupWorkspacePath: string; @@ -129,40 +129,29 @@ export class BackupFileService implements IBackupFileService { this.initialize(backupWorkspacePath); } - public initialize(backupWorkspacePath: string): void { + initialize(backupWorkspacePath: string): void { this.backupWorkspacePath = backupWorkspacePath; this.ready = this.init(); } - public get backupEnabled(): boolean { - return !!this.backupWorkspacePath; // Hot exit requires a backup path - } - private init(): TPromise { const model = new BackupFilesModel(); - if (!this.backupEnabled) { - return TPromise.as(model); - } - return model.resolve(this.backupWorkspacePath); } - public hasBackups(): TPromise { + hasBackups(): TPromise { return this.ready.then(model => { return model.count() > 0; }); } - public loadBackupResource(resource: Uri): TPromise { + loadBackupResource(resource: Uri): TPromise { return this.ready.then(model => { - const backupResource = this.toBackupResource(resource); - if (!backupResource) { - return void 0; - } // Return directly if we have a known backup with that resource + const backupResource = this.toBackupResource(resource); if (model.has(backupResource)) { return backupResource; } @@ -171,17 +160,13 @@ export class BackupFileService implements IBackupFileService { }); } - public backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise { + backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise { if (this.isShuttingDown) { return TPromise.as(void 0); } return this.ready.then(model => { const backupResource = this.toBackupResource(resource); - if (!backupResource) { - return void 0; - } - if (model.has(backupResource, versionId)) { return void 0; // return early if backup version id matches requested one } @@ -195,12 +180,9 @@ export class BackupFileService implements IBackupFileService { }); } - public discardResourceBackup(resource: Uri): TPromise { + discardResourceBackup(resource: Uri): TPromise { return this.ready.then(model => { const backupResource = this.toBackupResource(resource); - if (!backupResource) { - return void 0; - } return this.ioOperationQueues.queueFor(backupResource).queue(() => { return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource)); @@ -208,26 +190,21 @@ export class BackupFileService implements IBackupFileService { }); } - public discardAllWorkspaceBackups(): TPromise { + discardAllWorkspaceBackups(): TPromise { this.isShuttingDown = true; return this.ready.then(model => { - if (!this.backupEnabled) { - return void 0; - } - return pfs.del(this.backupWorkspacePath).then(() => model.clear()); }); } - public getWorkspaceFileBackups(): TPromise { + getWorkspaceFileBackups(): TPromise { return this.ready.then(model => { const readPromises: TPromise[] = []; model.get().forEach(fileBackup => { readPromises.push( - readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000) - .then(Uri.parse) + readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000).then(Uri.parse) ); }); @@ -235,7 +212,7 @@ export class BackupFileService implements IBackupFileService { }); } - public resolveBackupContent(backup: Uri): TPromise { + resolveBackupContent(backup: Uri): TPromise { return this.fileService.resolveStreamContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(content => { // Add a filter method to filter out everything until the meta marker @@ -258,11 +235,7 @@ export class BackupFileService implements IBackupFileService { }); } - public toBackupResource(resource: Uri): Uri { - if (!this.backupEnabled) { - return null; - } - + toBackupResource(resource: Uri): Uri { return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, this.hashPath(resource))); } @@ -270,3 +243,63 @@ export class BackupFileService implements IBackupFileService { return crypto.createHash('md5').update(resource.fsPath).digest('hex'); } } + +export class InMemoryBackupFileService implements IBackupFileService { + + _serviceBrand: any; + + private backups: Map = new Map(); + + hasBackups(): TPromise { + return TPromise.as(this.backups.size > 0); + } + + loadBackupResource(resource: Uri): TPromise { + const backupResource = this.toBackupResource(resource); + if (this.backups.has(backupResource.toString())) { + return TPromise.as(backupResource); + } + + return TPromise.as(void 0); + } + + backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise { + const backupResource = this.toBackupResource(resource); + this.backups.set(backupResource.toString(), content); + + return TPromise.as(void 0); + } + + resolveBackupContent(backupResource: Uri): TPromise { + const snapshot = this.backups.get(backupResource.toString()); + if (snapshot) { + return TPromise.as(createTextBufferFactoryFromSnapshot(snapshot)); + } + + return TPromise.as(void 0); + } + + getWorkspaceFileBackups(): TPromise { + return TPromise.as(keys(this.backups).map(key => Uri.parse(key))); + } + + discardResourceBackup(resource: Uri): TPromise { + this.backups.delete(this.toBackupResource(resource).toString()); + + return TPromise.as(void 0); + } + + discardAllWorkspaceBackups(): TPromise { + this.backups.clear(); + + return TPromise.as(void 0); + } + + toBackupResource(resource: Uri): Uri { + return Uri.file(path.join(resource.scheme, this.hashPath(resource))); + } + + private hashPath(resource: Uri): string { + return crypto.createHash('md5').update(resource.fsPath).digest('hex'); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 3af98297733..0dcce603c79 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -12,7 +12,7 @@ import * as os from 'os'; import * as fs from 'fs'; import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; -import Uri from 'vs/base/common/uri'; +import { URI as Uri } from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; @@ -39,7 +39,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.cre class TestBackupFileService extends BackupFileService { constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) { - const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); + const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); super(workspaceBackupPath, fileService); } diff --git a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts index fcb3ae878be..d9382085f1b 100644 --- a/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts @@ -5,42 +5,38 @@ 'use strict'; -import { getPathLabel } from 'vs/base/common/labels'; -import { IDisposable, IReference, dispose } from 'vs/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; +import { mergeSort } from 'vs/base/common/arrays'; +import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { ResourceFileEdit, ResourceTextEdit, WorkspaceEdit, isResourceFileEdit, isResourceTextEdit } from 'vs/editor/common/modes'; +import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IProgress, IProgressRunner, emptyProgressRunner } from 'vs/platform/progress/common/progress'; +import { ILogService } from 'vs/platform/log/common/log'; +import { emptyProgressRunner, IProgress, IProgressRunner } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ILabelService } from 'vs/platform/label/common/label'; abstract class Recording { static start(fileService: IFileService): Recording { let _changes = new Set(); - let stop: IDisposable; - - stop = fileService.onFileChanges(event => { - for (const change of event.changes) { - if (change.type === FileChangeType.UPDATED) { - _changes.add(change.resource.toString()); - } - } + let subscription = fileService.onAfterOperation(e => { + _changes.add(e.resource.toString()); }); return { - stop() { return dispose(stop); }, + stop() { return subscription.dispose(); }, hasChanged(resource) { return _changes.has(resource.toString()); } }; } @@ -49,30 +45,27 @@ abstract class Recording { abstract hasChanged(resource: URI): boolean; } -class EditTask implements IDisposable { +type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; - private _initialSelections: Selection[]; - private _endCursorSelection: Selection; - private get _model(): ITextModel { return this._modelReference.object.textEditorModel; } - private _modelReference: IReference; - private _edits: IIdentifiedSingleEditOperation[]; - private _newEol: EndOfLineSequence; +class ModelEditTask implements IDisposable { - constructor(modelReference: IReference) { - this._endCursorSelection = null; - this._modelReference = modelReference; + private readonly _model: ITextModel; + + protected _edits: IIdentifiedSingleEditOperation[]; + private _expectedModelVersionId: number | undefined; + protected _newEol: EndOfLineSequence; + + constructor(private readonly _modelReference: IReference) { + this._model = this._modelReference.object.textEditorModel; this._edits = []; } dispose() { - if (this._model) { - this._modelReference.dispose(); - this._modelReference = null; - } + dispose(this._modelReference); } addEdit(resourceEdit: ResourceTextEdit): void { - + this._expectedModelVersionId = resourceEdit.modelVersionId; for (const edit of resourceEdit.edits) { if (typeof edit.eol === 'number') { // honor eol-change @@ -91,20 +84,18 @@ class EditTask implements IDisposable { } } + validate(): ValidationResult { + if (typeof this._expectedModelVersionId === 'undefined' || this._model.getVersionId() === this._expectedModelVersionId) { + return { canApply: true }; + } + return { canApply: false, reason: this._model.uri }; + } + apply(): void { if (this._edits.length > 0) { - - this._edits = this._edits.map((value, index) => ({ value, index })).sort((a, b) => { - let ret = Range.compareRangesUsingStarts(a.value.range, b.value.range); - if (ret === 0) { - ret = a.index - b.index; - } - return ret; - }).map(element => element.value); - - this._initialSelections = this._getInitialSelections(); + this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); this._model.pushStackElement(); - this._model.pushEditOperations(this._initialSelections, this._edits, (edits) => this._getEndCursorSelections(edits)); + this._model.pushEditOperations([], this._edits, () => []); this._model.pushStackElement(); } if (this._newEol !== undefined) { @@ -113,58 +104,29 @@ class EditTask implements IDisposable { this._model.pushStackElement(); } } - - protected _getInitialSelections(): Selection[] { - const firstRange = this._edits[0].range; - const initialSelection = new Selection( - firstRange.startLineNumber, - firstRange.startColumn, - firstRange.endLineNumber, - firstRange.endColumn - ); - return [initialSelection]; - } - - private _getEndCursorSelections(inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] { - let relevantEditIndex = 0; - for (let i = 0; i < inverseEditOperations.length; i++) { - const editRange = inverseEditOperations[i].range; - for (let j = 0; j < this._initialSelections.length; j++) { - const selectionRange = this._initialSelections[j]; - if (Range.areIntersectingOrTouching(editRange, selectionRange)) { - relevantEditIndex = i; - break; - } - } - } - - const srcRange = inverseEditOperations[relevantEditIndex].range; - this._endCursorSelection = new Selection( - srcRange.endLineNumber, - srcRange.endColumn, - srcRange.endLineNumber, - srcRange.endColumn - ); - return [this._endCursorSelection]; - } - - getEndCursorSelection(): Selection { - return this._endCursorSelection; - } - } -class SourceModelEditTask extends EditTask { +class EditorEditTask extends ModelEditTask { - private _knownInitialSelections: Selection[]; + private _editor: ICodeEditor; - constructor(modelReference: IReference, initialSelections: Selection[]) { + constructor(modelReference: IReference, editor: ICodeEditor) { super(modelReference); - this._knownInitialSelections = initialSelections; + this._editor = editor; } - protected _getInitialSelections(): Selection[] { - return this._knownInitialSelections; + apply(): void { + if (this._edits.length > 0) { + this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this._editor.pushUndoStop(); + this._editor.executeEdits('', this._edits); + this._editor.pushUndoStop(); + } + if (this._newEol !== undefined) { + this._editor.pushUndoStop(); + this._editor.getModel().pushEOL(this._newEol); + this._editor.pushUndoStop(); + } } } @@ -172,10 +134,8 @@ class BulkEditModel implements IDisposable { private _textModelResolverService: ITextModelService; private _edits = new Map(); - private _tasks: EditTask[]; - private _sourceModel: URI; - private _sourceSelections: Selection[]; - private _sourceModelTask: SourceModelEditTask; + private _editor: ICodeEditor; + private _tasks: ModelEditTask[]; private _progress: IProgress; constructor( @@ -185,9 +145,7 @@ class BulkEditModel implements IDisposable { progress: IProgress ) { this._textModelResolverService = textModelResolverService; - this._sourceModel = editor ? editor.getModel().uri : undefined; - this._sourceSelections = editor ? editor.getSelections() : undefined; - this._sourceModelTask = undefined; + this._editor = editor; this._progress = progress; edits.forEach(this.addEdit, this); @@ -206,7 +164,7 @@ class BulkEditModel implements IDisposable { array.push(edit); } - async prepare(): TPromise { + async prepare(): Promise { if (this._tasks) { throw new Error('illegal state - already prepared'); @@ -223,12 +181,11 @@ class BulkEditModel implements IDisposable { throw new Error(`Cannot load file ${key}`); } - let task: EditTask; - if (this._sourceModel && model.textEditorModel.uri.toString() === this._sourceModel.toString()) { - this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections); - task = this._sourceModelTask; + let task: ModelEditTask; + if (this._editor && this._editor.getModel().uri.toString() === model.textEditorModel.uri.toString()) { + task = new EditorEditTask(ref, this._editor); } else { - task = new EditTask(ref); + task = new ModelEditTask(ref); } value.forEach(edit => task.addEdit(edit)); @@ -243,14 +200,21 @@ class BulkEditModel implements IDisposable { return this; } - apply(): Selection { + validate(): ValidationResult { + for (const task of this._tasks) { + const result = task.validate(); + if (!result.canApply) { + return result; + } + } + return { canApply: true }; + } + + apply(): void { for (const task of this._tasks) { task.apply(); this._progress.report(undefined); } - return this._sourceModelTask - ? this._sourceModelTask.getEndCursorSelection() - : undefined; } } @@ -265,8 +229,11 @@ export class BulkEdit { constructor( editor: ICodeEditor, progress: IProgressRunner, + @ILogService private readonly _logService: ILogService, @ITextModelService private readonly _textModelService: ITextModelService, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly _textFileService: ITextFileService, + @ILabelService private readonly _uriLabelServie: ILabelService ) { this._editor = editor; this._progress = progress || emptyProgressRunner; @@ -292,7 +259,7 @@ export class BulkEdit { } } - async perform(): TPromise { + async perform(): Promise { let seen = new Set(); let total = 0; @@ -322,35 +289,48 @@ export class BulkEdit { this._progress.total(total); let progress: IProgress = { report: _ => this._progress.worked(1) }; - // do it. return the last selection computed - // by a text change (can be undefined then) - let res: Selection = undefined; + // do it. for (const group of groups) { if (isResourceFileEdit(group[0])) { await this._performFileEdits(group, progress); } else { - res = await this._performTextEdits(group, progress) || res; + await this._performTextEdits(group, progress); } } - return res; } private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { + this._logService.debug('_performFileEdits', JSON.stringify(edits)); for (const edit of edits) { - progress.report(undefined); + let options = edit.options || {}; + if (edit.newUri && edit.oldUri) { - await this._fileService.moveFile(edit.oldUri, edit.newUri, false); + // rename + if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + await this._textFileService.move(edit.oldUri, edit.newUri, options.overwrite); + } else if (!edit.newUri && edit.oldUri) { - await this._fileService.del(edit.oldUri, true); + // delete file + if (!options.ignoreIfNotExists || await this._fileService.existsFile(edit.oldUri)) { + await this._textFileService.delete(edit.oldUri, { useTrash: true, recursive: options.recursive }); + } + } else if (edit.newUri && !edit.oldUri) { - await this._fileService.createFile(edit.newUri, undefined, { overwrite: false }); + // create file + if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite }); } } } - private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): TPromise { + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { + this._logService.debug('_performTextEdits', JSON.stringify(edits)); const recording = Recording.start(this._fileService); const model = new BulkEditModel(this._textModelService, this._editor, edits, progress); @@ -359,7 +339,7 @@ export class BulkEdit { const conflicts = edits .filter(edit => recording.hasChanged(edit.resource)) - .map(edit => getPathLabel(edit.resource)); + .map(edit => this._uriLabelServie.getUriLabel(edit.resource, true)); recording.stop(); @@ -368,9 +348,13 @@ export class BulkEdit { throw new Error(localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', '))); } - const selection = await model.apply(); + const validationResult = model.validate(); + if (validationResult.canApply === false) { + throw new Error(`${validationResult.reason.toString()} has changed in the meantime`); + } + + await model.apply(); model.dispose(); - return selection; } } @@ -379,10 +363,13 @@ export class BulkEditService implements IBulkEditService { _serviceBrand: any; constructor( + @ILogService private readonly _logService: ILogService, @IModelService private readonly _modelService: IModelService, @IEditorService private readonly _editorService: IEditorService, @ITextModelService private readonly _textModelService: ITextModelService, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly _textFileService: ITextFileService, + @ILabelService private readonly _labelService: ILabelService ) { } @@ -413,14 +400,17 @@ export class BulkEditService implements IBulkEditService { } } - const bulkEdit = new BulkEdit(options.editor, options.progress, this._textModelService, this._fileService); + const bulkEdit = new BulkEdit(options.editor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService); bulkEdit.add(edits); - return bulkEdit.perform().then(selection => { - return { - selection, - ariaSummary: bulkEdit.ariaMessage() - }; - }); + + return TPromise.wrap(bulkEdit.perform().then(() => { + return { ariaSummary: bulkEdit.ariaMessage() }; + }, err => { + // console.log('apply FAILED'); + // console.log(err); + this._logService.error(err); + throw err; + })); } } diff --git a/src/vs/workbench/services/codeEditor/browser/codeEditorService.ts b/src/vs/workbench/services/codeEditor/browser/codeEditorService.ts index 455bb51553b..0a65846ead6 100644 --- a/src/vs/workbench/services/codeEditor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/codeEditor/browser/codeEditorService.ts @@ -11,7 +11,7 @@ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TPromise } from 'vs/base/common/winjs.base'; -import { TextEditorOptions } from '../../../common/editor'; +import { TextEditorOptions } from 'vs/workbench/common/editor'; import { ScrollType } from 'vs/editor/common/editorCommon'; export class CodeEditorService extends CodeEditorServiceImpl { diff --git a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts index 5bb46155fc9..33e539305f4 100644 --- a/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/services/configuration/common/configurationExtensionPoint.ts @@ -28,7 +28,7 @@ const configurationEntrySchema: IJSONSchema = { type: 'object', additionalProperties: { anyOf: [ - { $ref: 'http://json-schema.org/draft-04/schema#' }, + { $ref: 'http://json-schema.org/draft-07/schema#' }, { type: 'object', properties: { @@ -46,6 +46,28 @@ const configurationEntrySchema: IJSONSchema = { nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.") ], description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.") + }, + enumDescriptions: { + type: 'array', + items: { + type: 'string', + }, + description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values') + }, + markdownEnumDescription: { + type: 'array', + items: { + type: 'string', + }, + description: nls.localize('scope.markdownEnumDescription', 'Descriptions for enum values in the markdown format.') + }, + markdownDescription: { + type: 'string', + description: nls.localize('scope.markdownDescription', 'The description in the markdown format.') + }, + deprecationMessage: { + type: 'string', + description: nls.localize('scope.deprecationMessage', 'If set, the property is marked as deprecated and the given message is shown as as explanation.') } } } @@ -106,7 +128,8 @@ configurationExtPoint.setHandler(extensions => { validateProperties(configuration, extension); - configuration.id = extension.description.uuid || extension.description.id; + configuration.id = node.id || extension.description.id || extension.description.uuid; + configuration.contributedByExtension = true; configuration.title = configuration.title || extension.description.displayName || extension.description.id; configurations.push(configuration); } @@ -154,7 +177,6 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten } else { propertyConfiguration.scope = ConfigurationScope.WINDOW; } - propertyConfiguration.notMultiRootAdopted = !(extension.description.isBuiltin || (Array.isArray(extension.description.keywords) && extension.description.keywords.indexOf('multi-root ready') !== -1)); } } let subNodes = configuration.allOf; diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 0d0fda5d2c1..cb2eef4404d 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -8,11 +8,11 @@ import { equals } from 'vs/base/common/objects'; import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { ResourceMap } from 'vs/base/common/map'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export class WorkspaceConfigurationModelParser extends ConfigurationModelParser { @@ -101,18 +101,27 @@ export class FolderSettingsModelParser extends ConfigurationModelParser { } private parseWorkspaceSettings(rawSettings: any): void { - const rawWorkspaceSettings = {}; const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - for (let key in rawSettings) { - const scope = this.getScope(key, configurationProperties); - if (this.scopes.indexOf(scope) !== -1) { - rawWorkspaceSettings[key] = rawSettings[key]; - } - } + const rawWorkspaceSettings = this.filterByScope(rawSettings, configurationProperties, true); const configurationModel = this.parseRaw(rawWorkspaceSettings); this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); } + private filterByScope(properties: {}, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean): {} { + const result = {}; + for (let key in properties) { + if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) { + result[key] = this.filterByScope(properties[key], configurationProperties, false); + } else { + const scope = this.getScope(key, configurationProperties); + if (this.scopes.indexOf(scope) !== -1) { + result[key] = properties[key]; + } + } + } + return result; + } + private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { const propertySchema = configurationProperties[key]; return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW; diff --git a/src/vs/workbench/services/configuration/common/jsonEditing.ts b/src/vs/workbench/services/configuration/common/jsonEditing.ts index d7e3ba6d347..6d7987d324a 100644 --- a/src/vs/workbench/services/configuration/common/jsonEditing.ts +++ b/src/vs/workbench/services/configuration/common/jsonEditing.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export const IJSONEditingService = createDecorator('jsonEditingService'); -export enum JSONEditingErrorCode { +export const enum JSONEditingErrorCode { /** * Error when trying to write and save to the file while it is dirty in the editor. diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index a47582c4722..f5eb365ece7 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createHash } from 'crypto'; import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; import * as pfs from 'vs/base/node/pfs'; @@ -124,7 +125,7 @@ export class WorkspaceConfiguration extends Disposable { } function isFolderConfigurationFile(resource: URI): boolean { - const name = paths.basename(resource.path); + const name = resources.basename(resource); return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(p => p === name);// only workspace config files } @@ -192,7 +193,7 @@ export abstract class AbstractFolderConfiguration extends Disposable implements private parseContents(contents: { resource: URI, value: string }[]): void { for (const content of contents) { - const name = paths.basename(content.resource.path); + const name = resources.basename(content.resource); if (name === `${FOLDER_SETTINGS_NAME}.json`) { this._folderSettingsModelParser.parse(content.value); } else { @@ -215,7 +216,7 @@ export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration { constructor(folder: URI, configFolderRelativePath: string, workbenchState: WorkbenchState) { super(folder, workbenchState); - this.folderConfigurationPath = URI.file(paths.join(this.folder.fsPath, configFolderRelativePath)); + this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath); } protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> { @@ -248,7 +249,7 @@ export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration { c({ resource, isDirectory: true, - children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; }) + children: children.map(child => { return { resource: resources.joinPath(resource, child) }; }) }); } }); @@ -264,7 +265,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) { super(folder, workbenchState, from); - this.folderConfigurationPath = folder.with({ path: paths.join(this.folder.path, configFolderRelativePath) }); + this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); } @@ -284,7 +285,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura } }).then(null, err => [] /* never fail this call */); - return bulkContentFetchromise.then(() => TPromise.join(workspaceFilePathToConfiguration).then(result => collections.values(result))); + return bulkContentFetchromise.then(() => TPromise.join(collections.values(workspaceFilePathToConfiguration))); } private handleWorkspaceFileEvents(event: FileChangesEvent): void { @@ -295,7 +296,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura for (let i = 0, len = events.length; i < len; i++) { const resource = events[i].resource; - const basename = paths.basename(resource.path); + const basename = resources.basename(resource); const isJson = paths.extname(basename) === '.json'; const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && basename === this.configFolderRelativePath); @@ -337,7 +338,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura return paths.normalize(relative(this.folderConfigurationPath.fsPath, resource.fsPath)); } } else { - if (paths.isEqualOrParent(resource.path, this.folderConfigurationPath.path, true /* ignorecase */)) { + if (resources.isEqualOrParent(resource, this.folderConfigurationPath)) { return paths.normalize(relative(this.folderConfigurationPath.path, resource.path)); } } @@ -489,4 +490,4 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat } return TPromise.as(null); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/configuration/node/configurationEditingService.ts b/src/vs/workbench/services/configuration/node/configurationEditingService.ts index ffea6529bbf..3124d92e563 100644 --- a/src/vs/workbench/services/configuration/node/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/node/configurationEditingService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as json from 'vs/base/common/json'; import * as encoding from 'vs/base/node/encoding'; import * as strings from 'vs/base/common/strings'; @@ -27,12 +27,12 @@ import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIG import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextModel } from 'vs/editor/common/model'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -export enum ConfigurationEditingErrorCode { +export const enum ConfigurationEditingErrorCode { /** * Error when trying to write a configuration key that is not registered. @@ -132,7 +132,7 @@ export class ConfigurationEditingService { @ITextModelService private textModelResolverService: ITextModelService, @ITextFileService private textFileService: ITextFileService, @INotificationService private notificationService: INotificationService, - @ICommandService private commandService: ICommandService, + @IPreferencesService private preferencesService: IPreferencesService, @IEditorService private editorService: IEditorService ) { this.queue = new Queue(); @@ -248,16 +248,16 @@ export class ConfigurationEditingService { private openSettings(operation: IConfigurationEditOperation): void { switch (operation.target) { case ConfigurationTarget.USER: - this.commandService.executeCommand('workbench.action.openGlobalSettings'); + this.preferencesService.openGlobalSettings(true); break; case ConfigurationTarget.WORKSPACE: - this.commandService.executeCommand('workbench.action.openWorkspaceSettings'); + this.preferencesService.openWorkspaceSettings(true); break; case ConfigurationTarget.WORKSPACE_FOLDER: if (operation.resource) { const workspaceFolder = this.contextService.getWorkspaceFolder(operation.resource); if (workspaceFolder) { - this.commandService.executeCommand('_workbench.action.openFolderSettings', workspaceFolder); + this.preferencesService.openFolderSettings(workspaceFolder.uri, true); } } break; @@ -371,7 +371,7 @@ export class ConfigurationEditingService { return false; } const parseErrors: json.ParseError[] = []; - json.parse(model.getValue(), parseErrors, { allowTrailingComma: true }); + json.parse(model.getValue(), parseErrors); return parseErrors.length > 0; } diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index f304dc4c546..782c8d13dc9 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { dirname, basename } from 'path'; import * as assert from 'vs/base/common/assert'; @@ -16,8 +16,8 @@ import { Queue } from 'vs/base/common/async'; import { stat, writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform'; import { IFileService } from 'vs/platform/files/common/files'; -import { isLinux } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration'; @@ -26,7 +26,7 @@ import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSetti import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { createHash } from 'crypto'; -import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -38,9 +38,9 @@ import { JSONEditingService } from 'vs/workbench/services/configuration/node/jso import { Schemas } from 'vs/base/common/network'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; +import { isEqual } from 'vs/base/common/resources'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -80,7 +80,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this._register(this.userConfiguration.onDidChangeConfiguration(() => this.onUserConfigurationChanged())); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); - this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(e => this.registerConfigurationSchemas())); + this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); this.workspaceEditingQueue = new Queue(); @@ -131,7 +131,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { switch (this.getWorkbenchState()) { case WorkbenchState.FOLDER: - return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier); + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && isEqual(workspaceIdentifier, this.workspace.folders[0].uri); case WorkbenchState.WORKSPACE: return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id; } @@ -295,9 +295,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this._configuration.keys(); } - initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { + initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration, postInitialisationTask: () => void = () => null): TPromise { return this.createWorkspace(arg) - .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace)); + .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask)); } acquireFileService(fileService: IFileService): void { @@ -322,7 +322,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this.jsonEditingService = instantiationService.createInstance(JSONEditingService); } - private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise { + private createWorkspace(arg: IWorkspaceIdentifier | URI | IWindowConfiguration): TPromise { if (isWorkspaceIdentifier(arg)) { return this.createMulitFolderWorkspace(arg); } @@ -340,20 +340,34 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat .then(() => { const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(workspaceConfigPath.fsPath))); const workspaceId = workspaceIdentifier.id; - const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService); - return new Workspace(workspaceId, workspaceName, workspaceFolders, workspaceConfigPath); + return new Workspace(workspaceId, workspaceFolders, workspaceConfigPath); }); } - private createSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise { - const folderPath = URI.file(singleFolderWorkspaceIdentifier); - return stat(folderPath.fsPath) - .then(workspaceStat => { - const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead! - const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex'); - const folder = URI.file(folderPath.fsPath); - return new Workspace(id, getBaseLabel(folder), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime); - }); + private createSingleFolderWorkspace(folder: URI): TPromise { + if (folder.scheme === Schemas.file) { + return stat(folder.fsPath) + .then(workspaceStat => { + let ctime: number; + if (isLinux) { + ctime = workspaceStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! + } else if (isMacintosh) { + ctime = workspaceStat.birthtime.getTime(); // macOS: birthtime is fine to use as is + } else if (isWindows) { + if (typeof workspaceStat.birthtimeMs === 'number') { + ctime = Math.floor(workspaceStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897) + } else { + ctime = workspaceStat.birthtime.getTime(); + } + } + + const id = createHash('md5').update(folder.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + return new Workspace(id, toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime); + }); + } else { + const id = createHash('md5').update(folder.toString()).digest('hex'); + return TPromise.as(new Workspace(id, toWorkspaceFolders([{ uri: folder.toString() }]), null)); + } } private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise { @@ -361,7 +375,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return TPromise.as(new Workspace(id)); } - private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): TPromise { + private updateWorkspaceAndInitializeConfiguration(workspace: Workspace, postInitialisationTask: () => void): TPromise { const hasWorkspaceBefore = !!this.workspace; let previousState: WorkbenchState; let previousWorkspacePath: string; @@ -377,6 +391,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } return this.initializeConfiguration().then(() => { + + postInitialisationTask(); // Post initialisation task should be run before triggering events. + // Trigger changes after configuration initialization so that configuration is up to date. if (hasWorkspaceBefore) { const newState = this.getWorkbenchState(); @@ -667,15 +684,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } return {}; } - - private pathEquals(path1: string, path2: string): boolean { - if (!isLinux) { - path1 = path1.toLowerCase(); - path2 = path2.toLowerCase(); - } - - return path1 === path2; - } } interface IExportedConfigurationNode { @@ -727,7 +735,7 @@ export class DefaultConfigurationExportHelper { const processProperty = (name: string, prop: IConfigurationPropertySchema) => { const propDetails: IExportedConfigurationNode = { name, - description: prop.description, + description: prop.description || prop.markdownDescription || '', default: prop.default, type: prop.type }; @@ -736,8 +744,8 @@ export class DefaultConfigurationExportHelper { propDetails.enum = prop.enum; } - if (prop.enumDescriptions) { - propDetails.enumDescriptions = prop.enumDescriptions; + if (prop.enumDescriptions || prop.markdownEnumDescriptions) { + propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions; } settings.push(propDetails); diff --git a/src/vs/workbench/services/configuration/node/jsonEditingService.ts b/src/vs/workbench/services/configuration/node/jsonEditingService.ts index 2c47d99e7c6..8bbb81dd700 100644 --- a/src/vs/workbench/services/configuration/node/jsonEditingService.ts +++ b/src/vs/workbench/services/configuration/node/jsonEditingService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as json from 'vs/base/common/json'; import * as encoding from 'vs/base/node/encoding'; import * as strings from 'vs/base/common/strings'; @@ -95,7 +95,7 @@ export class JSONEditingService implements IJSONEditingService { private hasParseErrors(model: ITextModel): boolean { const parseErrors: json.ParseError[] = []; - json.parse(model.getValue(), parseErrors, { allowTrailingComma: true }); + json.parse(model.getValue(), parseErrors); return parseErrors.length > 0; } diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 2a397b9df49..c998789df2f 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -9,7 +9,7 @@ import { join } from 'vs/base/common/paths'; import { Registry } from 'vs/platform/registry/common/platform'; import { FolderSettingsModelParser, WorkspaceConfigurationChangeEvent, StandaloneConfigurationModelParser, AllKeysConfigurationChangeEvent, Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -30,7 +30,8 @@ suite('FolderSettingsModelParser', () => { 'FolderSettingsModelParser.resource': { 'type': 'string', 'default': 'isSet', - scope: ConfigurationScope.RESOURCE + scope: ConfigurationScope.RESOURCE, + overridable: true }, 'FolderSettingsModelParser.application': { 'type': 'string', @@ -57,6 +58,14 @@ suite('FolderSettingsModelParser', () => { assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); + test('parse overridable resource settings', () => { + const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]); + + testObject.parse(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } })); + + assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource' } }, 'identifiers': ['json'] }]); + }); + test('reprocess folder settings excludes application setting', () => { const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); @@ -106,7 +115,7 @@ suite('WorkspaceConfigurationChangeEvent', () => { configurationChangeEvent.change(['window.restoreWindows'], URI.file('folder2')); configurationChangeEvent.telemetryData(ConfigurationTarget.WORKSPACE, {}); - let testObject = new WorkspaceConfigurationChangeEvent(configurationChangeEvent, new Workspace('id', 'name', + let testObject = new WorkspaceConfigurationChangeEvent(configurationChangeEvent, new Workspace('id', [new WorkspaceFolder({ index: 0, name: '1', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: '2', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: '3', uri: URI.file('folder3') })])); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 0e42aaccf0c..40fb45b9674 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -38,6 +38,7 @@ import { mkdirp } from 'vs/base/node/pfs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CommandService } from 'vs/workbench/services/commands/common/commandService'; +import { URI } from 'vs/base/common/uri'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -103,7 +104,7 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(IEnvironmentService, environmentService); const workspaceService = new WorkspaceService(environmentService); instantiationService.stub(IWorkspaceContextService, workspaceService); - return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => { + return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : URI.file(workspaceDir)).then(() => { instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index a38560e14b4..00d78fef9fe 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -10,7 +10,7 @@ import * as sinon from 'sinon'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { Registry } from 'vs/platform/registry/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -87,7 +87,7 @@ suite('WorkspaceContextService - Folder', () => { const globalSettingsFile = path.join(parentDir, 'settings.json'); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); workspaceContextService = new WorkspaceService(environmentService); - return (workspaceContextService).initialize(folderDir); + return (workspaceContextService).initialize(URI.file(folderDir)); }); }); @@ -124,11 +124,11 @@ suite('WorkspaceContextService - Folder', () => { }); test('isCurrentWorkspace() => true', () => { - assert.ok(workspaceContextService.isCurrentWorkspace(workspaceResource)); + assert.ok(workspaceContextService.isCurrentWorkspace(URI.file(workspaceResource))); }); test('isCurrentWorkspace() => false', () => { - assert.ok(!workspaceContextService.isCurrentWorkspace(workspaceResource + 'abc')); + assert.ok(!workspaceContextService.isCurrentWorkspace(URI.file(workspaceResource + 'abc'))); }); }); @@ -247,7 +247,7 @@ suite('WorkspaceContextService - Workspace', () => { const addedFolders = [{ uri: URI.file(path.join(workspaceDir, 'd')) }, { uri: URI.file(path.join(workspaceDir, 'c')) }]; return testObject.addFolders(addedFolders) .then(() => { - assert.ok(target.calledOnce); + assert.equal(target.callCount, 1, `Should be called only once but called ${target.callCount} times`); const actual = target.args[0][0]; assert.deepEqual(actual.added.map(r => r.uri.toString()), addedFolders.map(a => a.uri.toString())); assert.deepEqual(actual.removed, []); @@ -445,7 +445,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeConfiguration(target); - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(target.callCount, 3); @@ -474,7 +474,7 @@ suite('WorkspaceService - Initialization', () => { fs.writeFileSync(path.join(parentResource, '1', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue" }'); - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue'); assert.equal(target.callCount, 4); @@ -548,7 +548,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a folder workspace from a folder workspace with no configuration changes', () => { - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { fs.writeFileSync(globalSettingsFile, '{ "initialization.testSetting1": "userValue" }'); @@ -560,7 +560,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeWorkspaceFolders(target); testObject.onDidChangeConfiguration(target); - return testObject.initialize(path.join(parentResource, '2')) + return testObject.initialize(URI.file(path.join(parentResource, '2'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'userValue'); assert.equal(target.callCount, 1); @@ -576,7 +576,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a folder workspace from a folder workspace with configuration changes', () => { - return testObject.initialize(path.join(parentResource, '1')) + return testObject.initialize(URI.file(path.join(parentResource, '1'))) .then(() => { const target = sinon.spy(); @@ -586,7 +586,7 @@ suite('WorkspaceService - Initialization', () => { testObject.onDidChangeConfiguration(target); fs.writeFileSync(path.join(parentResource, '2', '.vscode', 'settings.json'), '{ "initialization.testSetting1": "workspaceValue2" }'); - return testObject.initialize(path.join(parentResource, '2')) + return testObject.initialize(URI.file(path.join(parentResource, '2'))) .then(() => { assert.equal(testObject.getValue('initialization.testSetting1'), 'workspaceValue2'); assert.equal(target.callCount, 2); @@ -601,7 +601,7 @@ suite('WorkspaceService - Initialization', () => { test('initialize a multi folder workspace from a folder workspacce triggers change events in the right order', () => { const folderDir = path.join(parentResource, '1'); - return testObject.initialize(folderDir) + return testObject.initialize(URI.file(folderDir)) .then(() => { const target = sinon.spy(); @@ -666,7 +666,7 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IEnvironmentService, environmentService); - return workspaceService.initialize(folderDir).then(() => { + return workspaceService.initialize(URI.file(folderDir)).then(() => { const fileService = new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); instantiationService.stub(IFileService, fileService); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index ec8a9eab100..e6ae183d5ec 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -13,9 +13,20 @@ export const IConfigurationResolverService = createDecorator): IStringDictionary; - resolveAny(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary): T; - executeCommandVariables(value: any, variables: IStringDictionary): TPromise>; + resolve(folder: IWorkspaceFolder, value: string): string; + resolve(folder: IWorkspaceFolder, value: string[]): string[]; + resolve(folder: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; + + /** + * Recursively resolves all variables in the given config and returns a copy of it with substituted values. + * Command variables are only substituted if a "commandValueMapping" dictionary is given and if it contains an entry for the command. + */ + resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any; + + /** + * Recursively resolves all variables (including commands) in the given config and returns a copy of it with substituted values. + * If a "variables" dictionary (with names -> command ids) is given, + * command variables are first mapped through it before being resolved. + */ + resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise; } diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index a0ca4baff20..9d6cce04087 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -3,39 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; +import * as platform from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; import { toResource } from 'vs/workbench/common/editor'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IStringDictionary, size } from 'vs/base/common/collections'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IProcessEnvironment } from 'vs/base/common/platform'; -import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; -export class ConfigurationResolverService implements IConfigurationResolverService { - _serviceBrand: any; - private resolver: VariableResolver; +export class ConfigurationResolverService extends AbstractVariableResolverService { constructor( - envVariables: IProcessEnvironment, + envVariables: platform.IProcessEnvironment, @IEditorService editorService: IEditorService, @IEnvironmentService environmentService: IEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @ICommandService private commandService: ICommandService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService ) { - this.resolver = new VariableResolver({ + super({ getFolderUri: (folderName: string): uri => { const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); return folder ? folder.uri : undefined; @@ -82,21 +79,34 @@ export class ConfigurationResolverService implements IConfigurationResolverServi }, envVariables); } - public resolve(root: IWorkspaceFolder, value: string): string; - public resolve(root: IWorkspaceFolder, value: string[]): string[]; - public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; - public resolve(root: IWorkspaceFolder, value: any): any { - return this.resolver.resolveAny(root ? root.uri : undefined, value); - } + public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise { - public resolveAny(root: IWorkspaceFolder, value: any, commandValueMapping?: IStringDictionary): any { - return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping); + // then substitute remaining variables in VS Code core + config = this.resolveAny(folder, config); + + // now evaluate command variables (which might have a UI) + return this.executeCommandVariables(config, variables).then(commandValueMapping => { + + if (!commandValueMapping) { // cancelled by user + return null; + } + + // finally substitute evaluated command variables (if there are any) + if (size(commandValueMapping) > 0) { + return this.resolveAny(folder, config, commandValueMapping); + } else { + return config; + } + }); } /** - * Finds and executes all command variables (see #6569) + * Finds and executes all command variables in the given configuration and returns their values as a dictionary. + * Please note: this method does not substitute the command variables (so the configuration is not modified). + * The returned dictionary can be passed to "resolvePlatform" for the substitution. + * See #6569. */ - public executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { + private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { if (!configuration) { return TPromise.as(null); @@ -131,22 +141,22 @@ export class ConfigurationResolverService implements IConfigurationResolverServi let cancelled = false; const commandValueMapping: IStringDictionary = Object.create(null); - const factory: { (): TPromise }[] = commands.map(interactiveVariable => { + const factory: { (): TPromise }[] = commands.map(commandVariable => { return () => { - let commandId = variableToCommandMap ? variableToCommandMap[interactiveVariable] : null; + let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : null; if (!commandId) { // Just launch any command if the interactive variable is not contributed by the adapter #12735 - commandId = interactiveVariable; + commandId = commandVariable; } return this.commandService.executeCommand(commandId, configuration).then(result => { if (typeof result === 'string') { - commandValueMapping[interactiveVariable] = result; + commandValueMapping[commandVariable] = result; } else if (isUndefinedOrNull(result)) { cancelled = true; } else { - throw new Error(localize('stringsOnlySupported', "Command {0} did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandId)); + throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable)); } }); }; diff --git a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts index 9c118dc6be8..b91edb96392 100644 --- a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts @@ -5,14 +5,18 @@ import * as paths from 'vs/base/common/paths'; import * as types from 'vs/base/common/types'; +import * as objects from 'vs/base/common/objects'; import { IStringDictionary } from 'vs/base/common/collections'; import { relative } from 'path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { localize } from 'vs/nls'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { TPromise } from 'vs/base/common/winjs.base'; -export interface IVariableAccessor { +export interface IVariableResolveContext { getFolderUri(folderName: string): uri | undefined; getWorkspaceFolderCount(): number; getConfigurationValue(folderUri: uri, section: string): string | undefined; @@ -22,47 +26,78 @@ export interface IVariableAccessor { getLineNumber(): string; } -export class VariableResolver { +export class AbstractVariableResolverService implements IConfigurationResolverService { static VARIABLE_REGEXP = /\$\{(.*?)\}/g; - private envVariables: IProcessEnvironment; + _serviceBrand: any; constructor( - private accessor: IVariableAccessor, - envVariables: IProcessEnvironment + private _context: IVariableResolveContext, + private _envVariables: IProcessEnvironment = process.env ) { if (isWindows) { - this.envVariables = Object.create(null); - Object.keys(envVariables).forEach(key => { - this.envVariables[key.toLowerCase()] = envVariables[key]; + this._envVariables = Object.create(null); + Object.keys(_envVariables).forEach(key => { + this._envVariables[key.toLowerCase()] = _envVariables[key]; }); - } else { - this.envVariables = envVariables; } } - resolveAny(folderUri: uri, value: any, commandValueMapping?: IStringDictionary): any { + public resolve(root: IWorkspaceFolder, value: string): string; + public resolve(root: IWorkspaceFolder, value: string[]): string[]; + public resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; + public resolve(root: IWorkspaceFolder, value: any): any { + return this.recursiveResolve(root ? root.uri : undefined, value); + } + + public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { + + const result = objects.deepClone(config) as any; + + // hoist platform specific attributes to top level + if (isWindows && result.windows) { + Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); + } else if (isMacintosh && result.osx) { + Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); + } else if (isLinux && result.linux) { + Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); + } + + // delete all platform specific sections + delete result.windows; + delete result.osx; + delete result.linux; + + // substitute all variables recursively in string values + return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping); + } + + public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise { + throw new Error('resolveWithCommands not implemented.'); + } + + private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary): any { if (types.isString(value)) { - return this.resolve(folderUri, value, commandValueMapping); + return this.resolveString(folderUri, value, commandValueMapping); } else if (types.isArray(value)) { - return value.map(s => this.resolveAny(folderUri, s, commandValueMapping)); + return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping)); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); Object.keys(value).forEach(key => { - const resolvedKey = this.resolve(folderUri, key, commandValueMapping); - result[resolvedKey] = this.resolveAny(folderUri, value[key], commandValueMapping); + const resolvedKey = this.resolveString(folderUri, key, commandValueMapping); + result[resolvedKey] = this.recursiveResolve(folderUri, value[key], commandValueMapping); }); return result; } return value; } - resolve(folderUri: uri, value: string, commandValueMapping: IStringDictionary): string { + private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary): string { - const filePath = this.accessor.getFilePath(); + const filePath = this._context.getFilePath(); - return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => { + return value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { let argument: string; const parts = variable.split(':'); @@ -78,7 +113,7 @@ export class VariableResolver { if (isWindows) { argument = argument.toLowerCase(); } - const env = this.envVariables[argument]; + const env = this._envVariables[argument]; if (types.isString(env)) { return env; } @@ -89,7 +124,7 @@ export class VariableResolver { case 'config': if (argument) { - const config = this.accessor.getConfigurationValue(folderUri, argument); + const config = this._context.getConfigurationValue(folderUri, argument); if (types.isUndefinedOrNull(config)) { throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument)); } @@ -120,7 +155,7 @@ export class VariableResolver { case 'workspaceFolderBasename': case 'relativeFile': if (argument) { - const folder = this.accessor.getFolderUri(argument); + const folder = this._context.getFolderUri(argument); if (folder) { folderUri = folder; } else { @@ -128,7 +163,7 @@ export class VariableResolver { } } if (!folderUri) { - if (this.accessor.getWorkspaceFolderCount() > 1) { + if (this._context.getWorkspaceFolderCount() > 1) { throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match)); } throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match)); @@ -167,14 +202,14 @@ export class VariableResolver { return paths.basename(folderUri.fsPath); case 'lineNumber': - const lineNumber = this.accessor.getLineNumber(); + const lineNumber = this._context.getLineNumber(); if (lineNumber) { return lineNumber; } throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match)); case 'selectedText': - const selectedText = this.accessor.getSelectedText(); + const selectedText = this._context.getSelectedText(); if (selectedText) { return selectedText; } @@ -203,7 +238,7 @@ export class VariableResolver { return basename.slice(0, basename.length - paths.extname(basename).length); case 'execPath': - const ep = this.accessor.getExecPath(); + const ep = this._context.getExecPath(); if (ep) { return ep; } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 57ba15ae14c..ed2493dfdd6 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import { TPromise } from 'vs/base/common/winjs.base'; import { IConfigurationService, getConfigurationValue, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; @@ -14,6 +14,7 @@ import { ConfigurationResolverService } from 'vs/workbench/services/configuratio import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { Disposable } from 'vs/base/common/lifecycle'; suite('Configuration Resolver Service', () => { let configurationResolverService: IConfigurationResolverService; @@ -239,29 +240,25 @@ suite('Configuration Resolver Service', () => { assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz')); }); - test('interactive variable simple', () => { + test('a single command variable', () => { + const configuration = { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': '${command:interactiveVariable1}', + 'processId': '${command:command1}', 'port': 5858, 'sourceMaps': false, 'outDir': null }; - const interactiveVariables = Object.create(null); - interactiveVariables['interactiveVariable1'] = 'command1'; - interactiveVariables['interactiveVariable2'] = 'command2'; - configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => { - - const result = configurationResolverService.resolveAny(undefined, configuration, mapping); + return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': 'command1', + 'processId': 'command1-result', 'port': 5858, 'sourceMaps': false, 'outDir': null @@ -271,43 +268,96 @@ suite('Configuration Resolver Service', () => { }); }); - test('interactive variable complex', () => { + test('an old style command variable', () => { const configuration = { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': '${command:interactiveVariable1}', - 'port': '${command:interactiveVariable2}', + 'processId': '${command:commandVariable1}', + 'port': 5858, 'sourceMaps': false, - 'outDir': 'src/${command:interactiveVariable2}', - 'env': { - 'processId': '__${command:interactiveVariable2}__', - } + 'outDir': null }; - const interactiveVariables = Object.create(null); - interactiveVariables['interactiveVariable1'] = 'command1'; - interactiveVariables['interactiveVariable2'] = 'command2'; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; - configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => { - - const result = configurationResolverService.resolveAny(undefined, configuration, mapping); + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': 'command1', - 'port': 'command2', + 'processId': 'command1-result', + 'port': 5858, 'sourceMaps': false, - 'outDir': 'src/command2', + 'outDir': null + }); + + assert.equal(1, mockCommandService.callCount); + }); + }); + + test('multiple new and old-style command variables', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${command:commandVariable1}', + 'pid': '${command:command2}', + 'sourceMaps': false, + 'outDir': 'src/${command:command2}', + 'env': { + 'processId': '__${command:command2}__', + } + }; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; + + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'command1-result', + 'pid': 'command2-result', + 'sourceMaps': false, + 'outDir': 'src/command2-result', 'env': { - 'processId': '__command2__', + 'processId': '__command2-result__', } }); assert.equal(2, mockCommandService.callCount); }); }); + + test('a command variable that relies on resolved env vars', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${command:commandVariable1}', + 'value': '${env:key1}' + }; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; + + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'Value for key1', + 'value': 'Value for key1' + }); + + assert.equal(1, mockCommandService.callCount); + }); + }); }); @@ -342,9 +392,17 @@ class MockCommandService implements ICommandService { public _serviceBrand: any; public callCount = 0; - onWillExecuteCommand = () => ({ dispose: () => { } }); + onWillExecuteCommand = () => Disposable.None; public executeCommand(commandId: string, ...args: any[]): TPromise { this.callCount++; - return TPromise.as(commandId); + + let result = `${commandId}-result`; + if (args.length >= 1) { + if (args[0] && args[0].value) { + result = args[0].value; + } + } + + return TPromise.as(result); } } diff --git a/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts b/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts index 5c323ca2409..160513801ef 100644 --- a/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts +++ b/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts @@ -12,37 +12,43 @@ import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; - -import { remote, webFrame } from 'electron'; +import { webFrame } from 'electron'; import { unmnemonicLabel } from 'vs/base/common/labels'; import { Event, Emitter } from 'vs/base/common/event'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IContextMenuDelegate, ContextSubMenu, IEvent } from 'vs/base/browser/contextmenu'; +import { IContextMenuDelegate, ContextSubMenu, IContextMenuEvent } from 'vs/base/browser/contextmenu'; +import { once } from 'vs/base/common/functional'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; +import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; -export class ContextMenuService implements IContextMenuService { +export class ContextMenuService extends Disposable implements IContextMenuService { - public _serviceBrand: any; - private _onDidContextMenu = new Emitter(); + _serviceBrand: any; + + private _onDidContextMenu = this._register(new Emitter()); + get onDidContextMenu(): Event { return this._onDidContextMenu.event; } constructor( @INotificationService private notificationService: INotificationService, @ITelemetryService private telemetryService: ITelemetryService, @IKeybindingService private keybindingService: IKeybindingService ) { + super(); } - public get onDidContextMenu(): Event { - return this._onDidContextMenu.event; - } - - public showContextMenu(delegate: IContextMenuDelegate): void { + showContextMenu(delegate: IContextMenuDelegate): void { delegate.getActions().then(actions => { - if (!actions.length) { - return TPromise.as(null); - } + if (actions.length) { + const onHide = once(() => { + if (delegate.onHide) { + delegate.onHide(undefined); + } - return TPromise.timeout(0).then(() => { // https://github.com/Microsoft/vscode/issues/3638 - const menu = this.createMenu(delegate, actions); + this._onDidContextMenu.fire(); + }); + + const menu = this.createMenu(delegate, actions, onHide); const anchor = delegate.getAnchor(); let x: number, y: number; @@ -61,63 +67,74 @@ export class ContextMenuService implements IContextMenuService { x *= zoom; y *= zoom; - menu.popup(remote.getCurrentWindow(), { x: Math.floor(x), y: Math.floor(y), positioningItem: delegate.autoSelectFirstItem ? 0 : void 0 }); - this._onDidContextMenu.fire(); - if (delegate.onHide) { - delegate.onHide(undefined); - } - }); - }); - } - - private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[]): Electron.Menu { - const menu = new remote.Menu(); - const actionRunner = delegate.actionRunner || new ActionRunner(); - - entries.forEach(e => { - if (e instanceof Separator) { - menu.append(new remote.MenuItem({ type: 'separator' })); - } else if (e instanceof ContextSubMenu) { - const submenu = new remote.MenuItem({ - submenu: this.createMenu(delegate, e.entries), - label: unmnemonicLabel(e.label) + popup(menu, { + x: Math.floor(x), + y: Math.floor(y), + positioningItem: delegate.autoSelectFirstItem ? 0 : void 0, + onHide: () => onHide() }); - - menu.append(submenu); - } else { - const options: Electron.MenuItemConstructorOptions = { - label: unmnemonicLabel(e.label), - checked: !!e.checked || !!e.radio, - type: !!e.checked ? 'checkbox' : !!e.radio ? 'radio' : void 0, - enabled: !!e.enabled, - click: (menuItem, win, event) => { - this.runAction(actionRunner, e, delegate, event); - } - }; - - const keybinding = !!delegate.getKeyBinding ? delegate.getKeyBinding(e) : this.keybindingService.lookupKeybinding(e.id); - if (keybinding) { - const electronAccelerator = keybinding.getElectronAccelerator(); - if (electronAccelerator) { - options.accelerator = electronAccelerator; - } else { - const label = keybinding.getLabel(); - if (label) { - options.label = `${options.label} [${label}]`; - } - } - } - - const item = new remote.MenuItem(options); - - menu.append(item); } }); - - return menu; } - private runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IEvent): void { + private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[], onHide: () => void): IContextMenuItem[] { + const actionRunner = delegate.actionRunner || new ActionRunner(); + + return entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide)); + } + + private createMenuItem(delegate: IContextMenuDelegate, entry: IAction | ContextSubMenu, actionRunner: IActionRunner, onHide: () => void): IContextMenuItem { + + // Separator + if (entry instanceof Separator) { + return { type: 'separator' } as IContextMenuItem; + } + + // Submenu + if (entry instanceof ContextSubMenu) { + return { + label: unmnemonicLabel(entry.label), + submenu: this.createMenu(delegate, entry.entries, onHide) + } as IContextMenuItem; + } + + // Normal Menu Item + else { + const item: IContextMenuItem = { + label: unmnemonicLabel(entry.label), + checked: !!entry.checked || !!entry.radio, + type: !!entry.checked ? 'checkbox' : !!entry.radio ? 'radio' : void 0, + enabled: !!entry.enabled, + click: event => { + + // To preserve pre-electron-2.x behaviour, we first trigger + // the onHide callback and then the action. + // Fixes https://github.com/Microsoft/vscode/issues/45601 + onHide(); + + // Run action which will close the menu + this.runAction(actionRunner, entry, delegate, event); + } + }; + + const keybinding = !!delegate.getKeyBinding ? delegate.getKeyBinding(entry) : this.keybindingService.lookupKeybinding(entry.id); + if (keybinding) { + const electronAccelerator = keybinding.getElectronAccelerator(); + if (electronAccelerator) { + item.accelerator = electronAccelerator; + } else { + const label = keybinding.getLabel(); + if (label) { + item.label = `${item.label} [${label}]`; + } + } + } + + return item; + } + } + + private runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): void { /* __GDPR__ "workbenchActionExecuted" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -129,6 +146,6 @@ export class ContextMenuService implements IContextMenuService { const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event; const res = actionRunner.run(actionToRun, context) || TPromise.as(null); - res.done(null, e => this.notificationService.error(e)); + res.then(null, e => this.notificationService.error(e)); } } diff --git a/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts b/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts index 627c221fa6c..71239496dad 100644 --- a/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts +++ b/src/vs/workbench/services/crashReporter/electron-browser/crashReporterService.ts @@ -5,7 +5,6 @@ 'use strict'; import * as nls from 'vs/nls'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { assign, deepClone } from 'vs/base/common/objects'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowsService } from 'vs/platform/windows/common/windows'; @@ -36,8 +35,9 @@ configurationRegistry.registerConfiguration({ 'properties': { 'telemetry.enableCrashReporter': { 'type': 'boolean', - 'description': nls.localize('telemetry.enableCrashReporting', "Enable crash reports to be sent to Microsoft.\nThis option requires restart to take effect."), - 'default': true + 'description': nls.localize('telemetry.enableCrashReporting', "Enable crash reports to be sent to a Microsoft online service. \nThis option requires restart to take effect."), + 'default': true, + 'tags': ['usesOnlineServices'] } } }); @@ -54,7 +54,7 @@ export const NullCrashReporterService: ICrashReporterService = { export class CrashReporterService implements ICrashReporterService { - public _serviceBrand: any; + _serviceBrand: any; private options: Electron.CrashReporterStartOptions; private isEnabled: boolean; @@ -97,8 +97,7 @@ export class CrashReporterService implements ICrashReporterService { // start crash reporter in the main process return this.windowsService.startCrashReporter(this.options); - }) - .done(null, onUnexpectedError); + }); } private getSubmitURL(): string { @@ -114,7 +113,7 @@ export class CrashReporterService implements ICrashReporterService { return submitURL; } - public getChildProcessStartOptions(name: string): Electron.CrashReporterStartOptions { + getChildProcessStartOptions(name: string): Electron.CrashReporterStartOptions { // Experimental crash reporting support for child processes on Mac only for now if (this.isEnabled && isMacintosh) { diff --git a/src/vs/workbench/services/decorations/browser/decorations.ts b/src/vs/workbench/services/decorations/browser/decorations.ts index e777db83119..3b5e58e2189 100644 --- a/src/vs/workbench/services/decorations/browser/decorations.ts +++ b/src/vs/workbench/services/decorations/browser/decorations.ts @@ -5,11 +5,11 @@ 'use strict'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const IDecorationsService = createDecorator('IFileDecorationsService'); @@ -26,13 +26,13 @@ export interface IDecoration { readonly tooltip: string; readonly labelClassName: string; readonly badgeClassName: string; - update(source?: string, data?: IDecorationData): IDecoration; + update(data: IDecorationData): IDecoration; } export interface IDecorationsProvider { readonly label: string; readonly onDidChange: Event; - provideDecorations(uri: URI): IDecorationData | TPromise; + provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Thenable; } export interface IResourceDecorationChangeEvent { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 513955a5b04..b2f151ec7bb 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event'; import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations'; import { TernarySearchTree } from 'vs/base/common/map'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { isThenable } from 'vs/base/common/async'; import { LinkedList } from 'vs/base/common/linkedList'; import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { IdGenerator } from 'vs/base/common/idGenerator'; -import { IIterator } from 'vs/base/common/iterator'; +import { Iterator } from 'vs/base/common/iterator'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; -import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; class DecorationRule { @@ -141,25 +141,12 @@ class DecorationStyles { labelClassName, badgeClassName, tooltip, - update: (source, insert) => { + update: (replace) => { let newData = data.slice(); - if (!source) { - // add -> just append - newData.push(insert); - - } else { - // remove/replace -> require a walk - for (let i = 0; i < newData.length; i++) { - if (newData[i].source === source) { - if (!insert) { - // remove - newData.splice(i, 1); - i--; - } else { - // replace - newData[i] = insert; - } - } + for (let i = 0; i < newData.length; i++) { + if (newData[i].source === replace.source) { + // replace + newData[i] = replace; } } return this.asDecoration(newData, onlyChildren); @@ -174,13 +161,13 @@ class DecorationStyles { }); } - cleanUp(iter: IIterator): void { + cleanUp(iter: Iterator): void { // remove every rule for which no more // decoration (data) is kept. this isn't cheap let usedDecorations = new Set(); for (let e = iter.next(); !e.done; e = iter.next()) { e.value.data.forEach((value, key) => { - if (!isThenable(value) && value) { + if (value && !(value instanceof DecorationDataRequest)) { usedDecorations.add(DecorationRule.keyOf(value)); } }); @@ -229,9 +216,16 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { } } +class DecorationDataRequest { + constructor( + readonly source: CancellationTokenSource, + readonly thenable: Thenable, + ) { } +} + class DecorationProviderWrapper { - readonly data = TernarySearchTree.forPaths | IDecorationData>(); + readonly data = TernarySearchTree.forPaths(); private readonly _dispoable: IDisposable; constructor( @@ -275,7 +269,7 @@ class DecorationProviderWrapper { item = this._fetchData(uri); } - if (item && !isThenable(item)) { + if (item && !(item instanceof DecorationDataRequest)) { // found something (which isn't pending anymore) callback(item, false); } @@ -285,7 +279,7 @@ class DecorationProviderWrapper { const iter = this.data.findSuperstr(key); if (iter) { for (let item = iter.next(); !item.done; item = iter.next()) { - if (item.value && !isThenable(item.value)) { + if (item.value && !(item.value instanceof DecorationDataRequest)) { callback(item.value, true); } } @@ -297,27 +291,28 @@ class DecorationProviderWrapper { // check for pending request and cancel it const pendingRequest = this.data.get(uri.toString()); - if (TPromise.is(pendingRequest)) { - pendingRequest.cancel(); + if (pendingRequest instanceof DecorationDataRequest) { + pendingRequest.source.cancel(); this.data.delete(uri.toString()); } - const dataOrThenable = this._provider.provideDecorations(uri); + const source = new CancellationTokenSource(); + const dataOrThenable = this._provider.provideDecorations(uri, source.token); if (!isThenable(dataOrThenable)) { // sync -> we have a result now return this._keepItem(uri, dataOrThenable); } else { // async -> we have a result soon - const request = TPromise.wrap(dataOrThenable).then(data => { + const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => { if (this.data.get(uri.toString()) === request) { this._keepItem(uri, data); } - }, err => { + }).catch(err => { if (!isPromiseCanceledError(err) && this.data.get(uri.toString()) === request) { this.data.delete(uri.toString()); } - }); + })); this.data.set(uri.toString(), request); return undefined; @@ -394,15 +389,13 @@ export class FileDecorationsService implements IDecorationsService { affectsResource() { return true; } }); - return { - dispose: () => { - // fire event that says 'yes' for any resource - // known to this provider. then dispose and remove it. - remove(); - this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) }); - wrapper.dispose(); - } - }; + return toDisposable(() => { + // fire event that says 'yes' for any resource + // known to this provider. then dispose and remove it. + remove(); + this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) }); + wrapper.dispose(); + }); } getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration { @@ -428,7 +421,7 @@ export class FileDecorationsService implements IDecorationsService { // result, maybe overwrite let result = this._decorationStyles.asDecoration(data, containsChildren); if (overwrite) { - return result.update(overwrite.source, overwrite); + return result.update(overwrite); } else { return result; } diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index f8469b69e23..0dd4f463a56 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -8,10 +8,10 @@ import * as assert from 'assert'; import { FileDecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event, toPromise, Emitter } from 'vs/base/common/event'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken } from 'vs/base/common/cancellation'; suite('DecorationsService', function () { @@ -34,7 +34,7 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return new TPromise(resolve => { + return new Promise(resolve => { setTimeout(() => resolve({ color: 'someBlue', tooltip: 'T' @@ -174,6 +174,7 @@ suite('DecorationsService', function () { test('Decorations not showing up for second root folder #48502', async function () { let cancelCount = 0; + let winjsCancelCount = 0; let callCount = 0; let provider = new class implements IDecorationsProvider { @@ -183,14 +184,17 @@ suite('DecorationsService', function () { label: string = 'foo'; - provideDecorations(uri: URI): TPromise { - return new TPromise(resolve => { + provideDecorations(uri: URI, token: CancellationToken): Promise { + + token.onCancellationRequested(() => { + cancelCount += 1; + }); + + return new Promise(resolve => { callCount += 1; setTimeout(() => { resolve({ letter: 'foo' }); }, 10); - }, () => { - cancelCount += 1; }); } }; @@ -204,6 +208,7 @@ suite('DecorationsService', function () { service.getDecoration(uri, false); assert.equal(cancelCount, 1); + assert.equal(winjsCancelCount, 0); assert.equal(callCount, 2); reg.dispose(); @@ -219,7 +224,7 @@ suite('DecorationsService', function () { if (uri.path.match(/hello$/)) { return { tooltip: 'FOO', weight: 17, bubble: true }; } else { - return new TPromise(_resolve => resolve = _resolve); + return new Promise(_resolve => resolve = _resolve); } } }); diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index df621fb08e1..c2c1ee52f58 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -13,6 +13,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { ILogService } from 'vs/platform/log/common/log'; interface IMassagedMessageBoxOptions { @@ -31,14 +32,16 @@ interface IMassagedMessageBoxOptions { export class DialogService implements IDialogService { - public _serviceBrand: any; + _serviceBrand: any; constructor( - @IWindowService private windowService: IWindowService - ) { - } + @IWindowService private windowService: IWindowService, + @ILogService private logService: ILogService + ) { } + + confirm(confirmation: IConfirmation): TPromise { + this.logService.trace('DialogService#confirm', confirmation.message); - public confirm(confirmation: IConfirmation): TPromise { const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); return this.windowService.showMessageBox(options).then(result => { @@ -86,7 +89,9 @@ export class DialogService implements IDialogService { return opts; } - public show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise { + show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise { + this.logService.trace('DialogService#show', message); + const { options, buttonIndexMap } = this.massageMessageBoxOptions({ message, buttons, diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index d5ba813c5c7..1766a4066d0 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -13,24 +13,22 @@ import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInpu import { Registry } from 'vs/platform/registry/common/platform'; import { ResourceMap } from 'vs/base/common/map'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; -import { getPathLabel } from 'vs/base/common/labels'; import { Event, once, Emitter } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { basename } from 'vs/base/common/paths'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/group/common/editorGroupsService'; -import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, ACTIVE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { coalesce } from 'vs/base/common/arrays'; import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, IEditorOpeningEvent, EditorGroupsServiceImpl, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { ILabelService } from 'vs/platform/label/common/label'; type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; @@ -65,9 +63,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { constructor( @IEditorGroupsService private editorGroupService: EditorGroupsServiceImpl, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @IInstantiationService private instantiationService: IInstantiationService, - @IEnvironmentService private environmentService: IEnvironmentService, + @ILabelService private labelService: ILabelService, @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService ) { @@ -235,15 +232,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { return this.doOpenEditor(targetGroup, editor, editorOptions); } - // Throw error for well known foreign resources (such as a http link) (TODO@ben remove me after this has been adopted) - const resourceInput = editor; - if (resourceInput.resource instanceof URI) { - const schema = resourceInput.resource.scheme; - if (schema === Schemas.http || schema === Schemas.https) { - return TPromise.wrapError(new Error('Invalid scheme http/https to open resource as editor. Use IOpenerService instead.')); - } - } - // Untyped Text Editor Support const textInput = editor; const typedInput = this.createInput(textInput); @@ -269,18 +257,13 @@ export class EditorService extends Disposable implements EditorServiceImpl { return group; } - // Group: Active Group - if (group === ACTIVE_GROUP) { - targetGroup = this.editorGroupService.activeGroup; - } - // Group: Side by Side - else if (group === SIDE_GROUP) { + if (group === SIDE_GROUP) { targetGroup = this.findSideBySideGroup(); } // Group: Specific Group - else if (typeof group === 'number') { + else if (typeof group === 'number' && group >= 0) { targetGroup = this.editorGroupService.getGroup(group); } @@ -487,8 +470,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Side by Side Support const resourceSideBySideInput = input; if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) { - const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource }); - const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource }); + const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource, forceFile: resourceSideBySideInput.forceFile }); + const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource, forceFile: resourceSideBySideInput.forceFile }); return new SideBySideEditorInput( resourceSideBySideInput.label || masterInput.getName(), @@ -501,9 +484,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Diff Editor Support const resourceDiffInput = input; if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { - const leftInput = this.createInput({ resource: resourceDiffInput.leftResource }); - const rightInput = this.createInput({ resource: resourceDiffInput.rightResource }); - const label = resourceDiffInput.label || localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput, this.workspaceContextService, this.environmentService), this.toDiffLabel(rightInput, this.workspaceContextService, this.environmentService)); + const leftInput = this.createInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); + const rightInput = this.createInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile }); + const label = resourceDiffInput.label || localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput), this.toDiffLabel(rightInput)); return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput); } @@ -527,13 +510,13 @@ export class EditorService extends Disposable implements EditorServiceImpl { label = basename(resourceInput.resource.fsPath); // derive the label from the path (but not for data URIs) } - return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding) as EditorInput; + return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.forceFile) as EditorInput; } return null; } - private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput { + private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string, forceFile?: boolean): ICachedEditorInput { if (EditorService.CACHE.has(resource)) { const input = EditorService.CACHE.get(resource); if (input instanceof ResourceEditorInput) { @@ -549,7 +532,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { let input: ICachedEditorInput; // File - if (this.fileService.canHandleResource(resource)) { + if (forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resource)) { input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService); } @@ -571,7 +554,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return input; } - private toDiffLabel(input: EditorInput, context: IWorkspaceContextService, environment: IEnvironmentService): string { + private toDiffLabel(input: EditorInput): string { const res = input.getResource(); // Do not try to extract any paths from simple untitled editors @@ -580,7 +563,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Otherwise: for diff labels prefer to see the path as part of the label - return getPathLabel(res.fsPath, context, environment); + return this.labelService.getUriLabel(res, true); } //#endregion @@ -600,18 +583,16 @@ export class DelegatingEditorService extends EditorService { constructor( @IEditorGroupsService editorGroupService: EditorGroupsServiceImpl, @IUntitledEditorService untitledEditorService: IUntitledEditorService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IInstantiationService instantiationService: IInstantiationService, - @IEnvironmentService environmentService: IEnvironmentService, + @ILabelService labelService: ILabelService, @IFileService fileService: IFileService, @IConfigurationService configurationService: IConfigurationService ) { super( editorGroupService, untitledEditorService, - workspaceContextService, instantiationService, - environmentService, + labelService, fileService, configurationService ); 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 f5c38e6dd49..b5c0d429999 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -9,7 +9,7 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import * as paths from 'vs/base/common/paths'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; @@ -28,7 +28,6 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; export class TestEditorControl extends BaseEditor { @@ -77,8 +76,8 @@ suite('Editor service', () => { const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); + const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); + const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -145,9 +144,9 @@ suite('Editor service', () => { const service: IEditorService = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); - const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3')); + const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-openEditors')); + const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors')); + const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-openEditors')); // Open editors return service.openEditors([{ editor: input }, { editor: otherInput }]).then(() => { @@ -259,7 +258,7 @@ suite('Editor service', () => { const ed = instantiationService.createInstance(MyEditor, 'my.editor'); - const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource')); + const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate')); const delegate = instantiationService.createInstance(DelegatingEditorService); delegate.setEditorOpenHandler((group: IEditorGroup, input: IEditorInput, options?: EditorOptions) => { assert.strictEqual(input, inp); @@ -283,7 +282,7 @@ suite('Editor service', () => { const service: IEditorService = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); + const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-close1')); const rootGroup = part.activeGroup; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -308,100 +307,6 @@ suite('Editor service', () => { }); }); - test('close editor does not dispose when editor opened in other group (diff input)', function () { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart, 'id', false); - part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); - - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); - const diffInput = new DiffEditorInput('name', 'description', input, otherInput); - - const rootGroup = part.activeGroup; - const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - - // Open input - return service.openEditor(diffInput, { pinned: true }).then(editor => { - return service.openEditor(diffInput, { pinned: true }, rightGroup).then(editor => { - - // Close input - return rootGroup.closeEditor(diffInput).then(() => { - assert.equal(diffInput.isDisposed(), false); - assert.equal(input.isDisposed(), false); - assert.equal(otherInput.isDisposed(), false); - - return rightGroup.closeEditor(diffInput).then(() => { - assert.equal(diffInput.isDisposed(), true); - assert.equal(input.isDisposed(), true); - assert.equal(otherInput.isDisposed(), true); - }); - }); - }); - }); - }); - - test('close editor disposes properly (diff input)', function () { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart, 'id', false); - part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); - - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); - const diffInput = new DiffEditorInput('name', 'description', input, otherInput); - - // Open input - return service.openEditor(diffInput, { pinned: true }).then(editor => { - - // Close input - return editor.group.closeEditor(diffInput).then(() => { - assert.equal(diffInput.isDisposed(), true); - assert.equal(otherInput.isDisposed(), true); - assert.equal(input.isDisposed(), true); - }); - }); - }); - - test('close editor disposes properly (diff input, left side still opened)', function () { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart, 'id', false); - part.create(document.createElement('div')); - part.layout(new Dimension(400, 300)); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); - - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); - const diffInput = new DiffEditorInput('name', 'description', input, otherInput); - - // Open input - return service.openEditor(diffInput, { pinned: true }).then(editor => { - return service.openEditor(input, { pinned: true }).then(editor => { - - // Close input - return editor.group.closeEditor(diffInput).then(() => { - assert.equal(diffInput.isDisposed(), true); - assert.equal(otherInput.isDisposed(), true); - assert.equal(input.isDisposed(), false); - }); - }); - }); - }); - test('open to the side', function () { const partInstantiator = workbenchInstantiationService(); @@ -413,8 +318,8 @@ suite('Editor service', () => { const service: IEditorService = testInstantiationService.createInstance(EditorService); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1')); - const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); + const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); + const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); const rootGroup = part.activeGroup; @@ -445,8 +350,8 @@ suite('Editor service', () => { const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2')); + const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); let activeEditorChangeEventFired = false; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -494,7 +399,7 @@ suite('Editor service', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - editor = await service.openEditor(input, { forceOpen: true }); + editor = await service.openEditor(input, { forceReload: true }); assertActiveEditorChangedEvent(false); assertVisibleEditorsChangedEvent(false); diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index d8bfa847f36..10cdd8756a0 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { Event } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export interface IExtensionDescription { readonly id: string; @@ -116,6 +116,8 @@ export class ExtensionPointContribution { } } +export const ExtensionHostLogFileName = 'exthost'; + export interface IExtensionService { _serviceBrand: any; diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index c21f0dd7858..4299179a2cd 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -120,7 +120,7 @@ export class ExtensionPoint implements IExtensionPoint { } const schemaId = 'vscode://schemas/vscode-extensions'; -const schema: IJSONSchema = { +export const schema = { properties: { engines: { type: 'object', @@ -220,6 +220,16 @@ const schema: IJSONSchema = { description: nls.localize('vscode.extension.activationEvents.workspaceContains', 'An activation event emitted whenever a folder is opened that contains at least a file matching the specified glob pattern.'), body: 'workspaceContains:${4:filePattern}' }, + { + label: 'onFileSystem', + description: nls.localize('vscode.extension.activationEvents.onFileSystem', 'An activation event emitted whenever a file or folder is accessed with the given scheme.'), + body: 'onFileSystem:${1:scheme}' + }, + { + label: 'onSearch', + description: nls.localize('vscode.extension.activationEvents.onSearch', 'An activation event emitted whenever a search is started in the folder with the given scheme.'), + body: 'onSearch:${7:scheme}' + }, { label: 'onView', body: 'onView:${5:viewId}', @@ -288,6 +298,15 @@ const schema: IJSONSchema = { pattern: EXTENSION_IDENTIFIER_PATTERN } }, + extensionPack: { + description: nls.localize('vscode.extension.contributes.extensionPack', "A set of extensions that can be installed together. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), + type: 'array', + uniqueItems: true, + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN + } + }, scripts: { type: 'object', properties: { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index dad9839b58b..5de1b8a967d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -8,9 +8,8 @@ import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as objects from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { isWindows, isLinux } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; import { findFreePort } from 'vs/base/node/ports'; import { ILifecycleService, ShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; @@ -20,24 +19,38 @@ import { ChildProcess, fork } from 'child_process'; import { ipcRenderer as ipc } from 'electron'; import product from 'vs/platform/node/product'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net'; import { createServer, Server, Socket } from 'net'; import { Event, Emitter, debounceEvent, mapEvent, anyEvent, fromNodeEventEmitter } from 'vs/base/common/event'; -import { IInitData, IWorkspaceData, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol'; +import { IInitData, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ICrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService'; import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService'; -import { isEqual } from 'vs/base/common/paths'; +import { isEqual } from 'vs/base/common/resources'; import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console'; import { getScopes } from 'vs/platform/configuration/common/configurationRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { timeout } from 'vs/base/common/async'; +import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/common/extensionHostProtocol'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { onUnexpectedError } from 'vs/base/common/errors'; -export class ExtensionHostProcessWorker { +export interface IExtensionHostStarter { + readonly onCrashed: Event<[number, string]>; + start(): TPromise; + getInspectPort(): number; + dispose(): void; +} + +export class ExtensionHostProcessWorker implements IExtensionHostStarter { private readonly _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onCrashed: Event<[number, string]> = this._onCrashed.event; @@ -62,6 +75,7 @@ export class ExtensionHostProcessWorker { constructor( private readonly _extensions: TPromise, + private readonly _extensionHostLogsLocation: URI, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @INotificationService private readonly _notificationService: INotificationService, @IWindowsService private readonly _windowsService: IWindowsService, @@ -72,12 +86,15 @@ export class ExtensionHostProcessWorker { @IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ICrashReporterService private readonly _crashReporterService: ICrashReporterService, - @ILogService private readonly _logService: ILogService + @ILogService private readonly _logService: ILogService, + @ILabelService private readonly _labelService: ILabelService ) { // handle extension host lifecycle a bit special when we know we are developing an extension that runs inside this._isExtensionDevHost = this._environmentService.isExtensionDevelopment; - this._isExtensionDevDebug = (typeof this._environmentService.debugExtensionHost.port === 'number'); - this._isExtensionDevDebugBrk = !!this._environmentService.debugExtensionHost.break; + const extDevLoc = this._environmentService.extensionDevelopmentLocationURI; + const debugOk = extDevLoc && extDevLoc.scheme === Schemas.file; + this._isExtensionDevDebug = debugOk && typeof this._environmentService.debugExtensionHost.port === 'number'; + this._isExtensionDevDebugBrk = debugOk && !!this._environmentService.debugExtensionHost.break; this._isExtensionDevTestFromCli = this._isExtensionDevHost && !!this._environmentService.extensionTestsPath && !this._environmentService.debugExtensionHost.break; this._lastExtensionHostError = null; @@ -96,11 +113,9 @@ export class ExtensionHostProcessWorker { const globalExitListener = () => this.terminate(); process.once('exit', globalExitListener); - this._toDispose.push({ - dispose: () => { - process.removeListener('exit', globalExitListener); - } - }); + this._toDispose.push(toDisposable(() => { + process.removeListener('exit', globalExitListener); + })); } public dispose(): void { @@ -111,15 +126,15 @@ export class ExtensionHostProcessWorker { // Close Ext Host Window Request if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) { - const extensionPaths = broadcast.payload as string[]; - if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) { + const extensionLocations = broadcast.payload as string[]; + if (Array.isArray(extensionLocations) && extensionLocations.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) { this._windowService.closeWindow(); } } if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) { const extensionPaths = broadcast.payload as string[]; - if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) { + if (Array.isArray(extensionPaths) && extensionPaths.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) { this._windowService.reloadWindow(); } } @@ -143,7 +158,8 @@ export class ExtensionHostProcessWorker { VERBOSE_LOGGING: true, VSCODE_IPC_HOOK_EXTHOST: pipeName, VSCODE_HANDLES_UNCAUGHT_ERRORS: true, - VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || product.quality !== 'stable' || this._environmentService.verbose) + VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || product.quality !== 'stable' || this._environmentService.verbose), + VSCODE_LOG_LEVEL: this._environmentService.verbose ? 'trace' : this._environmentService.log }), // We only detach the extension host on windows. Linux and Mac orphan by default // and detach under Linux and Mac create another process group. @@ -172,7 +188,7 @@ export class ExtensionHostProcessWorker { } // Run Extension Host as fork of current process - this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts); + this._extensionHostProcess = fork(getPathFromAmdModule(require, 'bootstrap-fork'), ['--type=extensionHost'], opts); // Catch all output coming from the extension host process type Output = { data: string, format: string[] }; @@ -193,13 +209,13 @@ export class ExtensionHostProcessWorker { }, 100); // Print out extension host output - onDebouncedOutput(data => { - const inspectorUrlIndex = !this._environmentService.isBuilt && data.data && data.data.indexOf('chrome-devtools://'); - if (inspectorUrlIndex >= 0) { - console.log(`%c[Extension Host] %cdebugger inspector at ${data.data.substr(inspectorUrlIndex)}`, 'color: blue', 'color: black'); + onDebouncedOutput(output => { + const inspectorUrlMatch = !this._environmentService.isBuilt && output.data && output.data.match(/ws:\/\/([^\s]+)/); + if (inspectorUrlMatch) { + console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color: black'); } else { console.group('Extension Host'); - console.log(data.data, ...data.format); + console.log(output.data, ...output.format); console.groupEnd(); } }); @@ -216,7 +232,7 @@ export class ExtensionHostProcessWorker { this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal)); // Notify debugger that we are ready to attach to the process if we run a development extension - if (this._isExtensionDevHost && portData.actual) { + if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug) { this._broadcastService.broadcast({ channel: EXTENSION_ATTACH_BROADCAST_CHANNEL, payload: { @@ -258,8 +274,8 @@ export class ExtensionHostProcessWorker { /** * Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name. */ - private _tryListenOnPipe(): TPromise { - return new TPromise((resolve, reject) => { + private _tryListenOnPipe(): Promise { + return new Promise((resolve, reject) => { const pipeName = generateRandomPipeName(); this._namedPipeServer = createServer(); @@ -274,15 +290,15 @@ export class ExtensionHostProcessWorker { /** * Find a free port if extension host debugging is enabled. */ - private _tryFindDebugPort(): TPromise<{ expected: number; actual: number }> { + private _tryFindDebugPort(): Promise<{ expected: number; actual: number }> { let expected: number; let startPort = 9333; if (typeof this._environmentService.debugExtensionHost.port === 'number') { startPort = expected = this._environmentService.debugExtensionHost.port; } else { - return TPromise.as({ expected: undefined, actual: 0 }); + return Promise.resolve({ expected: undefined, actual: 0 }); } - return new TPromise((c, e) => { + return new Promise(resolve => { return findFreePort(startPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */).then(port => { if (!port) { console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color: black'); @@ -296,14 +312,14 @@ export class ExtensionHostProcessWorker { console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color: black'); } } - return c({ expected, actual: port }); + return resolve({ expected, actual: port }); }); }); } - private _tryExtHostHandshake(): TPromise { + private _tryExtHostHandshake(): Promise { - return new TPromise((resolve, reject) => { + return new Promise((resolve, reject) => { // Wait for the extension host to connect to our named pipe // and wrap the socket in the message passing protocol @@ -325,7 +341,7 @@ export class ExtensionHostProcessWorker { // 1) wait for the incoming `ready` event and send the initialization data. // 2) wait for the incoming `initialized` event. - return new TPromise((resolve, reject) => { + return new Promise((resolve, reject) => { let handle = setTimeout(() => { reject('timeout'); @@ -333,13 +349,13 @@ export class ExtensionHostProcessWorker { const disposable = protocol.onMessage(msg => { - if (msg === 'ready') { + if (isMessageOfType(msg, MessageType.Ready)) { // 1) Extension Host is ready to receive messages, initialize it - this._createExtHostInitData().then(data => protocol.send(JSON.stringify(data))); + this._createExtHostInitData().then(data => protocol.send(Buffer.from(JSON.stringify(data)))); return; } - if (msg === 'initialized') { + if (isMessageOfType(msg, MessageType.Initialized)) { // 2) Extension Host is initialized clearTimeout(handle); @@ -348,7 +364,10 @@ export class ExtensionHostProcessWorker { disposable.dispose(); // release this promise - resolve(protocol); + // using a buffered message protocol here because between now + // and the first time a `then` executes some messages might be lost + // unless we immediately register a listener for `onMessage`. + resolve(new BufferedMessagePassingProtocol(protocol)); return; } @@ -361,29 +380,34 @@ export class ExtensionHostProcessWorker { } private _createExtHostInitData(): TPromise { - return TPromise.join([this._telemetryService.getTelemetryInfo(), this._extensions]).then(([telemetryInfo, extensionDescriptions]) => { - const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} }; - const r: IInitData = { - parentPid: process.pid, - environment: { - isExtensionDevelopmentDebug: this._isExtensionDevDebug, - appRoot: this._environmentService.appRoot, - appSettingsHome: this._environmentService.appSettingsHome, - disableExtensions: this._environmentService.disableExtensions, - extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath, - extensionTestsPath: this._environmentService.extensionTestsPath - }, - workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace(), - extensions: extensionDescriptions, - // Send configurations scopes only in development mode. - configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData, - telemetryInfo, - windowId: this._windowService.getCurrentWindowId(), - logLevel: this._logService.getLevel(), - logsPath: this._environmentService.logsPath - }; - return r; - }); + return TPromise.join([this._telemetryService.getTelemetryInfo(), this._extensions]) + .then(([telemetryInfo, extensionDescriptions]) => { + const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} }; + const workspace = this._contextService.getWorkspace(); + const r: IInitData = { + parentPid: process.pid, + environment: { + isExtensionDevelopmentDebug: this._isExtensionDevDebug, + appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : void 0, + appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : void 0, + extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, + extensionTestsPath: this._environmentService.extensionTestsPath + }, + workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { + configuration: workspace.configuration, + folders: workspace.folders, + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace) + }, + extensions: extensionDescriptions, + // Send configurations scopes only in development mode. + configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData, + telemetryInfo, + logLevel: this._logService.getLevel(), + logsLocation: this._extensionHostLogsLocation + }; + return r; + }); } private _logExtensionHostMessage(entry: IRemoteConsoleLog) { @@ -464,9 +488,7 @@ export class ExtensionHostProcessWorker { // Send the extension host a request to terminate itself // (graceful termination) - protocol.send({ - type: '__$terminate' - }); + protocol.send(createMessageOfType(MessageType.Terminate)); // Give the extension host 60s, after which we will // try to kill the process and release any resources @@ -507,7 +529,61 @@ export class ExtensionHostProcessWorker { } }); - event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false)); + event.veto(timeout(100 /* wait a bit for IPC to get delivered */).then(() => false)); } } } + +/** + * Will ensure no messages are lost from creation time until the first user of onMessage comes in. + */ +class BufferedMessagePassingProtocol implements IMessagePassingProtocol { + + private readonly _actual: IMessagePassingProtocol; + private _bufferedMessagesListener: IDisposable; + private _bufferedMessages: Buffer[]; + + constructor(actual: IMessagePassingProtocol) { + this._actual = actual; + this._bufferedMessages = []; + this._bufferedMessagesListener = this._actual.onMessage((buff) => this._bufferedMessages.push(buff)); + } + + public send(buffer: Buffer): void { + this._actual.send(buffer); + } + + public onMessage(listener: (e: Buffer) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable { + if (!this._bufferedMessages) { + // second caller gets nothing + return this._actual.onMessage(listener, thisArgs, disposables); + } + + // prepare result + const result = this._actual.onMessage(listener, thisArgs, disposables); + + // stop listening to buffered messages + this._bufferedMessagesListener.dispose(); + + // capture buffered messages + const bufferedMessages = this._bufferedMessages; + this._bufferedMessages = null; + + // it is important to deliver these messages after this call, but before + // other messages have a chance to be received (to guarantee in order delivery) + // that's why we're using here nextTick and not other types of timeouts + process.nextTick(() => { + // deliver buffered messages + while (bufferedMessages.length > 0) { + const msg = bufferedMessages.shift(); + try { + listener.call(thisArgs, msg); + } catch (e) { + onUnexpectedError(e); + } + } + }); + + return result; + } +} diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 78e9ab2545e..67953d609ca 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -12,14 +12,14 @@ import pkg from 'vs/platform/node/package'; import * as path from 'path'; import * as os from 'os'; import * as pfs from 'vs/base/node/pfs'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution, ActivationTimes, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; import { USER_MANIFEST_CACHE_FILE, BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions'; import { IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference, Translations, IRelaxedExtensionDescription } from 'vs/workbench/services/extensions/node/extensionPoints'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol'; @@ -27,8 +27,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { ExtensionHostProcessWorker, IExtensionHostStarter } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; @@ -39,21 +39,28 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import product from 'vs/platform/node/product'; import * as strings from 'vs/base/common/strings'; -import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { RPCProtocol, IRPCProtocolLogger, RequestInitiator } from 'vs/workbench/services/extensions/node/rpcProtocol'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { Schemas } from 'vs/base/common/network'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { isEqualOrParent } from 'vs/base/common/resources'; + +// Enable to see detailed message communication between window and extension host +const LOG_EXTENSION_HOST_COMMUNICATION = false; +const LOG_USE_COLORS = true; let _SystemExtensionsRoot: string = null; function getSystemExtensionsRoot(): string { if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); + _SystemExtensionsRoot = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'extensions')); } return _SystemExtensionsRoot; } let _ExtraDevSystemExtensionsRoot: string = null; function getExtraDevSystemExtensionsRoot(): string { if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', '.build', 'builtInExtensions')); + _ExtraDevSystemExtensionsRoot = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions')); } return _ExtraDevSystemExtensionsRoot; } @@ -94,9 +101,6 @@ class ExtraBuiltInExtensionResolver implements IExtensionResolver { } } -// Enable to see detailed message communication between window and extension host -const logExtensionHostCommunication = false; - function messageWithSource(source: string, message: string): string { if (source) { return `[${source}]: ${message}`; @@ -117,14 +121,14 @@ export class ExtensionHostProcessManager extends Disposable { private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; }; private _extensionHostProcessRPCProtocol: RPCProtocol; private readonly _extensionHostProcessCustomers: IDisposable[]; - private readonly _extensionHostProcessWorker: ExtensionHostProcessWorker; + private readonly _extensionHostProcessWorker: IExtensionHostStarter; /** * winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object. */ - private readonly _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>; + private _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>; constructor( - extensionHostProcessWorker: ExtensionHostProcessWorker, + extensionHostProcessWorker: IExtensionHostStarter, initialActivationEvents: string[], @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @@ -133,7 +137,6 @@ export class ExtensionHostProcessManager extends Disposable { this._extensionHostProcessFinishedActivateEvents = Object.create(null); this._extensionHostProcessRPCProtocol = null; this._extensionHostProcessCustomers = []; - this._extensionHostProcessProxy = null; this._extensionHostProcessWorker = extensionHostProcessWorker; this.onDidCrash = this._extensionHostProcessWorker.onCrashed; @@ -167,6 +170,7 @@ export class ExtensionHostProcessManager extends Disposable { errors.onUnexpectedError(err); } } + this._extensionHostProcessProxy = null; super.dispose(); } @@ -177,12 +181,17 @@ export class ExtensionHostProcessManager extends Disposable { private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape { - if (logExtensionHostCommunication || this._environmentService.logExtensionHostCommunication) { - protocol = asLoggingProtocol(protocol); + let logger: IRPCProtocolLogger = null; + if (LOG_EXTENSION_HOST_COMMUNICATION || this._environmentService.logExtensionHostCommunication) { + logger = new RPCLogger(); } - this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol); - const extHostContext: IExtHostContext = this._extensionHostProcessRPCProtocol; + this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol, logger); + const extHostContext: IExtHostContext = { + getProxy: (identifier: ProxyIdentifier): T => this._extensionHostProcessRPCProtocol.getProxy(identifier), + set: (identifier: ProxyIdentifier, instance: R): R => this._extensionHostProcessRPCProtocol.set(identifier, instance), + assertRegistered: (identifiers: ProxyIdentifier[]): void => this._extensionHostProcessRPCProtocol.assertRegistered(identifiers), + }; // Named customers const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers(); @@ -213,6 +222,11 @@ export class ExtensionHostProcessManager extends Disposable { return NO_OP_VOID_PROMISE; } return this._extensionHostProcessProxy.then((proxy) => { + if (!proxy) { + // this case is already covered above and logged. + // i.e. the extension host could not be started + return NO_OP_VOID_PROMISE; + } return proxy.value.$activateByEvent(activationEvent); }).then(() => { this._extensionHostProcessFinishedActivateEvents[activationEvent] = true; @@ -230,11 +244,15 @@ export class ExtensionHostProcessManager extends Disposable { } } +schema.properties.engines.properties.vscode.default = `^${pkg.version}`; + export class ExtensionService extends Disposable implements IExtensionService { + public _serviceBrand: any; private readonly _onDidRegisterExtensions: Emitter; + private readonly _extensionHostLogsLocation: URI; private _registry: ExtensionDescriptionRegistry; private readonly _installedExtensionsReady: Barrier; private readonly _isDev: boolean; @@ -261,6 +279,7 @@ export class ExtensionService extends Disposable implements IExtensionService { @IExtensionManagementService private extensionManagementService: IExtensionManagementService ) { super(); + this._extensionHostLogsLocation = URI.file(path.posix.join(this._environmentService.logsPath, `exthost${this._windowService.getCurrentWindowId()}`)); this._registry = null; this._installedExtensionsReady = new Barrier(); this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment; @@ -275,8 +294,8 @@ export class ExtensionService extends Disposable implements IExtensionService { this.startDelayed(lifecycleService); - if (this._environmentService.disableExtensions) { - this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All extensions are temporarily disabled. Reload the window to return to the previous state."), [{ + if (this._extensionEnablementService.allUserExtensionsDisabled) { + this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{ label: nls.localize('Reload', "Reload"), run: () => { this._windowService.reloadWindow(); @@ -353,7 +372,7 @@ export class ExtensionService extends Disposable implements IExtensionService { private _startExtensionHostProcess(initialActivationEvents: string[]): void { this._stopExtensionHostProcess(); - const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this.getExtensions()); + const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this.getExtensions(), this._extensionHostLogsLocation); const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, initialActivationEvents); extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal)); this._extensionHostProcessManagers.push(extHostProcessManager); @@ -481,9 +500,10 @@ export class ExtensionService extends Disposable implements IExtensionService { private _scanAndHandleExtensions(): void { - this._getRuntimeExtensions() - .then(runtimeExtensons => { - this._registry = new ExtensionDescriptionRegistry(runtimeExtensons); + this._scanExtensions() + .then(allExtensions => this._getRuntimeExtensions(allExtensions)) + .then(allExtensions => { + this._registry = new ExtensionDescriptionRegistry(allExtensions); let availableExtensions = this._registry.getAllExtensionDescriptions(); let extensionPoints = ExtensionsRegistry.getExtensionPoints(); @@ -501,100 +521,124 @@ export class ExtensionService extends Disposable implements IExtensionService { }); } - private _getRuntimeExtensions(): TPromise { + private _scanExtensions(): TPromise { const log = new Logger((severity, source, message) => { this._logOrShowMessage(severity, this._isDev ? messageWithSource(source, message) : message); }); - return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, log) + return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log) .then(({ system, user, development }) => { - return this._extensionEnablementService.getDisabledExtensions() - .then(disabledExtensions => { - let result: { [extensionId: string]: IExtensionDescription; } = {}; - let extensionsToDisable: IExtensionIdentifier[] = []; - let userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }]; - - system.forEach((systemExtension) => { - if (disabledExtensions.every(disabled => !areSameExtensions(disabled, systemExtension))) { - result[systemExtension.id] = systemExtension; - } - }); - - user.forEach((userExtension) => { - if (result.hasOwnProperty(userExtension.id)) { - log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); - } - if (disabledExtensions.every(disabled => !areSameExtensions(disabled, userExtension))) { - // Check if the extension is changed to system extension - let userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: userExtension.id }))[0]; - if (userMigratedSystemExtension) { - extensionsToDisable.push(userMigratedSystemExtension); - } else { - result[userExtension.id] = userExtension; - } - } - }); - - development.forEach(developedExtension => { - log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); - if (result.hasOwnProperty(developedExtension.id)) { - log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionLocation.fsPath, developedExtension.extensionLocation.fsPath)); - } - // Do not disable extensions under development - result[developedExtension.id] = developedExtension; - }); - - const runtimeExtensions = Object.keys(result).map(name => result[name]); - - this._telemetryService.publicLog('extensionsScanned', { - totalCount: runtimeExtensions.length, - disabledCount: disabledExtensions.length - }); - - if (extensionsToDisable.length) { - return this.extensionManagementService.getInstalled(LocalExtensionType.User) - .then(installed => { - const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e))); - return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled))); - }) - .then(() => { - this._storageService.store(BetterMergeDisabledNowKey, true); - return runtimeExtensions; - }); - } else { - return runtimeExtensions; - } - }); - }).then(extensions => this._updateEnableProposedApi(extensions)); + let result: { [extensionId: string]: IExtensionDescription; } = {}; + system.forEach((systemExtension) => { + result[systemExtension.id] = systemExtension; + }); + user.forEach((userExtension) => { + if (result.hasOwnProperty(userExtension.id)) { + log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + } + result[userExtension.id] = userExtension; + }); + development.forEach(developedExtension => { + log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); + if (result.hasOwnProperty(developedExtension.id)) { + log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionLocation.fsPath, developedExtension.extensionLocation.fsPath)); + } + result[developedExtension.id] = developedExtension; + }); + return Object.keys(result).map(name => result[name]); + }); } - private _updateEnableProposedApi(extensions: IExtensionDescription[]): IExtensionDescription[] { - const enableProposedApiForAll = !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0); - const enableProposedApiFor = this._environmentService.args['enable-proposed-api'] || []; - for (const extension of extensions) { - if (!isFalsyOrEmpty(product.extensionAllowedProposedApi) - && product.extensionAllowedProposedApi.indexOf(extension.id) >= 0 - ) { - // fast lane -> proposed api is available to all extensions - // that are listed in product.json-files - extension.enableProposedApi = true; + private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise { + return this._extensionEnablementService.getDisabledExtensions() + .then(disabledExtensions => { - } else if (extension.enableProposedApi && !extension.isBuiltin) { - if ( - !enableProposedApiForAll && - enableProposedApiFor.indexOf(extension.id) < 0 - ) { - extension.enableProposedApi = false; - console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`); + const result: { [extensionId: string]: IExtensionDescription; } = {}; + const extensionsToDisable: IExtensionIdentifier[] = []; + const userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }]; - } else { - // proposed api is available when developing or when an extension was explicitly - // spelled out via a command line argument - console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`); + const enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || []; + + const notFound = (id: string) => nls.localize('notFound', "Extension \`{0}\` cannot use PROPOSED API as it cannot be found", id); + + if (enableProposedApiFor.length) { + let allProposed = (enableProposedApiFor instanceof Array ? enableProposedApiFor : [enableProposedApiFor]); + allProposed.forEach(id => { + if (!allExtensions.some(description => description.id === id)) { + console.error(notFound(id)); + } + }); } + + const enableProposedApiForAll = !this._environmentService.isBuilt || + (!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong.indexOf('Insiders') >= 0) || + (enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args); + + for (const extension of allExtensions) { + const isExtensionUnderDevelopment = this._environmentService.isExtensionDevelopment && isEqualOrParent(extension.extensionLocation, this._environmentService.extensionDevelopmentLocationURI); + // Do not disable extensions under development + if (!isExtensionUnderDevelopment) { + if (disabledExtensions.some(disabled => areSameExtensions(disabled, extension))) { + continue; + } + } + + if (!extension.isBuiltin) { + // Check if the extension is changed to system extension + const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.id }))[0]; + if (userMigratedSystemExtension) { + extensionsToDisable.push(userMigratedSystemExtension); + continue; + } + } + result[extension.id] = this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor); + } + const runtimeExtensions = Object.keys(result).map(name => result[name]); + + this._telemetryService.publicLog('extensionsScanned', { + totalCount: runtimeExtensions.length, + disabledCount: disabledExtensions.length + }); + + if (extensionsToDisable.length) { + return this.extensionManagementService.getInstalled(LocalExtensionType.User) + .then(installed => { + const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e))); + return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled))); + }) + .then(() => { + this._storageService.store(BetterMergeDisabledNowKey, true); + return runtimeExtensions; + }); + } else { + return runtimeExtensions; + } + }); + } + + private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription { + if (!isFalsyOrEmpty(product.extensionAllowedProposedApi) + && product.extensionAllowedProposedApi.indexOf(extension.id) >= 0 + ) { + // fast lane -> proposed api is available to all extensions + // that are listed in product.json-files + extension.enableProposedApi = true; + + } else if (extension.enableProposedApi && !extension.isBuiltin) { + if ( + !enableProposedApiForAll && + enableProposedApiFor.indexOf(extension.id) < 0 + ) { + extension.enableProposedApi = false; + console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`); + + } else { + // proposed api is available when developing or when an extension was explicitly + // spelled out via a command line argument + console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`); } } - return extensions; + return extension; } private _handleExtensionPointMessage(msg: IMessage) { @@ -629,7 +673,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } } - private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise { + private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -664,7 +708,7 @@ export class ExtensionService extends Disposable implements IExtensionService { ); } - private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise { + private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -678,7 +722,7 @@ export class ExtensionService extends Disposable implements IExtensionService { return null; } - private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): TPromise { + private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): Promise { const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); @@ -695,7 +739,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } } - private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise { + private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise { if (input.devMode) { // Do not cache when running out of sources... return ExtensionScanner.scanExtensions(input, log); @@ -739,7 +783,7 @@ export class ExtensionService extends Disposable implements IExtensionService { return result; } - private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { + private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { const translationConfig: TPromise = platform.translationsConfigFile ? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => { @@ -768,10 +812,10 @@ export class ExtensionService extends Disposable implements IExtensionService { log ); - let finalBuiltinExtensions: TPromise = builtinExtensions; + let finalBuiltinExtensions: TPromise = TPromise.wrap(builtinExtensions); if (devMode) { - const builtInExtensionsFilePath = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'build', 'builtInExtensions.json')); + const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json')); const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8') .then(raw => JSON.parse(raw)); @@ -811,7 +855,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } const userExtensions = ( - environmentService.disableExtensions || !environmentService.extensionsPath + extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath ? TPromise.as([]) : this._scanExtensionsWithCache( windowService, @@ -824,13 +868,12 @@ export class ExtensionService extends Disposable implements IExtensionService { ); // Always load developed extensions while extensions development - const developedExtensions = ( - environmentService.isExtensionDevelopment - ? ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionDevelopmentPath, false, true, translations), log - ) - : TPromise.as([]) - ); + let developedExtensions = TPromise.as([]); + if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) { + developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions( + new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionDevelopmentLocationURI.fsPath, false, true, translations), log + ); + } return TPromise.join([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { const system = extensionDescriptions[0]; @@ -917,20 +960,60 @@ export class ExtensionService extends Disposable implements IExtensionService { } } -function asLoggingProtocol(protocol: IMessagePassingProtocol): IMessagePassingProtocol { +const colorTables = [ + ['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'], + ['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD'] +]; - protocol.onMessage(msg => { - console.log('%c[Extension \u2192 Window]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg); - }); - - return { - onMessage: protocol.onMessage, - - send(msg: any) { - protocol.send(msg); - console.log('%c[Window \u2192 Extension]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg); +function prettyWithoutArrays(data: any): any { + if (Array.isArray(data)) { + return data; + } + if (data && typeof data === 'object' && typeof data.toString === 'function') { + let result = data.toString(); + if (result !== '[object Object]') { + return result; } - }; + } + return data; +} + +function pretty(data: any): any { + if (Array.isArray(data)) { + return data.map(prettyWithoutArrays); + } + return prettyWithoutArrays(data); +} + +class RPCLogger implements IRPCProtocolLogger { + + private _totalIncoming = 0; + private _totalOutgoing = 0; + + private _log(direction: string, totalLength, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void { + data = pretty(data); + + const colorTable = colorTables[initiator]; + const color = LOG_USE_COLORS ? colorTable[req % colorTable.length] : '#000000'; + let args = [`%c[${direction}]%c[${strings.pad(totalLength, 7, ' ')}]%c[len: ${strings.pad(msgLength, 5, ' ')}]%c${strings.pad(req, 5, ' ')} - ${str}`, 'color: darkgreen', 'color: grey', 'color: grey', `color: ${color}`]; + if (/\($/.test(str)) { + args = args.concat(data); + args.push(')'); + } else { + args.push(data); + } + console.log.apply(console, args); + } + + logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void { + this._totalIncoming += msgLength; + this._log('Ext \u2192 Win', this._totalIncoming, msgLength, req, initiator, str, data); + } + + logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void { + this._totalOutgoing += msgLength; + this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data); + } } interface IExtensionCacheData { diff --git a/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts b/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts new file mode 100644 index 00000000000..ebe6207a9ee --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { localize } from 'vs/nls'; + +const FIVE_MINUTES = 5 * 60 * 1000; +const THIRTY_SECONDS = 30 * 1000; + +function isExtensionId(value: string): boolean { + return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value); +} + +export const IExtensionUrlHandler = createDecorator('inactiveExtensionUrlHandler'); + +export interface IExtensionUrlHandler { + readonly _serviceBrand: any; + registerExtensionHandler(extensionId: string, handler: IURLHandler): void; + unregisterExtensionHandler(extensionId: string): void; +} + +/** + * This class handles URLs which are directed towards inactive extensions. + * If a URL is directed towards an inactive extension, it buffers it, + * activates the extension and re-opens the URL once the extension registers + * a URL handler. If the extension never registers a URL handler, the urls + * will eventually be garbage collected. + * + * It also makes sure the user confirms opening URLs directed towards extensions. + */ +export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { + + readonly _serviceBrand: any; + + private extensionHandlers = new Map(); + private uriBuffer = new Map(); + private disposable: IDisposable; + + constructor( + @IURLService urlService: IURLService, + @IExtensionService private extensionService: IExtensionService, + @IDialogService private dialogService: IDialogService + ) { + const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS); + + this.disposable = combinedDisposable([ + urlService.registerHandler(this), + toDisposable(() => clearInterval(interval)) + ]); + } + + handleURL(uri: URI): TPromise { + if (!isExtensionId(uri.authority)) { + return TPromise.as(false); + } + + const extensionId = uri.authority; + const wasHandlerAvailable = this.extensionHandlers.has(extensionId); + + return this.extensionService.getExtensions().then(extensions => { + const extension = extensions.filter(e => e.id === extensionId)[0]; + + if (!extension) { + return TPromise.as(false); + } + + return this.dialogService.confirm({ + message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId), + detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}` + }).then(result => { + + if (!result.confirmed) { + return TPromise.as(true); + } + + const handler = this.extensionHandlers.get(extensionId); + if (handler) { + if (!wasHandlerAvailable) { + // forward it directly + return handler.handleURL(uri); + } + + // let the ExtensionUrlHandler instance handle this + return TPromise.as(false); + } + + // collect URI for eventual extension activation + const timestamp = new Date().getTime(); + let uris = this.uriBuffer.get(extensionId); + + if (!uris) { + uris = []; + this.uriBuffer.set(extensionId, uris); + } + + uris.push({ timestamp, uri }); + + // activate the extension + return this.extensionService.activateByEvent(`onUri:${extensionId}`) + .then(() => true); + }); + }); + } + + registerExtensionHandler(extensionId: string, handler: IURLHandler): void { + this.extensionHandlers.set(extensionId, handler); + + const uris = this.uriBuffer.get(extensionId) || []; + + for (const { uri } of uris) { + handler.handleURL(uri); + } + + this.uriBuffer.delete(extensionId); + } + + unregisterExtensionHandler(extensionId: string): void { + this.extensionHandlers.delete(extensionId); + } + + // forget about all uris buffered more than 5 minutes ago + private garbageCollect(): void { + const now = new Date().getTime(); + const uriBuffer = new Map(); + + this.uriBuffer.forEach((uris, extensionId) => { + uris = uris.filter(({ timestamp }) => now - timestamp < FIVE_MINUTES); + + if (uris.length > 0) { + uriBuffer.set(extensionId, uris); + } + }); + + this.uriBuffer = uriBuffer; + } + + dispose(): void { + this.disposable.dispose(); + this.extensionHandlers.clear(); + this.uriBuffer.clear(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/node/extensionManagementServerService.ts b/src/vs/workbench/services/extensions/node/extensionManagementServerService.ts new file mode 100644 index 00000000000..87bfffa3579 --- /dev/null +++ b/src/vs/workbench/services/extensions/node/extensionManagementServerService.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { localize } from 'vs/nls'; + +const localExtensionManagementServerAuthority: string = 'vscode-local'; + +export class ExtensionManagementServerService implements IExtensionManagementServerService { + + _serviceBrand: any; + + readonly extensionManagementServers: IExtensionManagementServer[]; + + constructor( + localExtensionManagementService: IExtensionManagementService + ) { + this.extensionManagementServers = [{ extensionManagementService: localExtensionManagementService, authority: localExtensionManagementServerAuthority, label: localize('local', "Local") }]; + } + + getExtensionManagementServer(location: URI): IExtensionManagementServer { + return this.extensionManagementServers[0]; + } + + getLocalExtensionManagementServer(): IExtensionManagementServer { + return this.extensionManagementServers[0]; + } +} + +export class SingleServerExtensionManagementServerService implements IExtensionManagementServerService { + + _serviceBrand: any; + + readonly extensionManagementServers: IExtensionManagementServer[]; + + constructor( + extensionManagementServer: IExtensionManagementServer + ) { + this.extensionManagementServers = [extensionManagementServer]; + } + + getExtensionManagementServer(location: URI): IExtensionManagementServer { + const authority = location.scheme === Schemas.file ? localExtensionManagementServerAuthority : location.authority; + return this.extensionManagementServers.filter(server => authority === server.authority)[0]; + } + + getLocalExtensionManagementServer(): IExtensionManagementServer { + return this.extensionManagementServers[0]; + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 8de75c2f9a6..4dba0393dcb 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -17,7 +17,7 @@ import * as semver from 'semver'; import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { groupByExtension, getGalleryExtensionId, getLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; const MANIFEST_FILE = 'package.json'; @@ -208,7 +208,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { * Parses original message bundle, returns null if the original message bundle is null. */ private static resolveOriginalMessageBundle(originalMessageBundle: string, errors: json.ParseError[]) { - return new TPromise<{ [key: string]: string; }>((c, e, p) => { + return new TPromise<{ [key: string]: string; }>((c, e) => { if (originalMessageBundle) { pfs.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); @@ -226,7 +226,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { * If the localized file is not present, returns null for the original and marks original as localized. */ private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string): TPromise<{ localized: string, original: string }> { - return new TPromise<{ localized: string, original: string }>((c, e, p) => { + return new TPromise<{ localized: string, original: string }>((c, e) => { function loop(basename: string, locale: string): void { let toCheck = `${basename}.nls.${locale}.json`; pfs.fileExists(toCheck).then(exists => { @@ -524,7 +524,7 @@ export class ExtensionScanner { /** * Scan a list of extensions defined in `absoluteFolderPath` */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): TPromise { + public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): Promise { const absoluteFolderPath = input.absoluteFolderPath; const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; @@ -615,4 +615,4 @@ export class ExtensionScanner { return []; }); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/node/lazyPromise.ts b/src/vs/workbench/services/extensions/node/lazyPromise.ts index 2b096574133..da83c8f4964 100644 --- a/src/vs/workbench/services/extensions/node/lazyPromise.ts +++ b/src/vs/workbench/services/extensions/node/lazyPromise.ts @@ -7,9 +7,7 @@ import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; -export class LazyPromise implements TPromise { - - private _onCancel: () => void; +export class LazyPromise implements Thenable { private _actual: TPromise; private _actualOk: ValueCallback; @@ -21,10 +19,7 @@ export class LazyPromise implements TPromise { private _hasErr: boolean; private _err: any; - private _isCanceled: boolean; - - constructor(onCancel: () => void) { - this._onCancel = onCancel; + constructor() { this._actual = null; this._actualOk = null; this._actualErr = null; @@ -32,7 +27,6 @@ export class LazyPromise implements TPromise { this._value = null; this._hasErr = false; this._err = null; - this._isCanceled = false; } private _ensureActual(): TPromise { @@ -40,7 +34,7 @@ export class LazyPromise implements TPromise { this._actual = new TPromise((c, e) => { this._actualOk = c; this._actualErr = e; - }, this._onCancel); + }); if (this._hasValue) { this._actualOk(this._value); @@ -54,7 +48,7 @@ export class LazyPromise implements TPromise { } public resolveOk(value: any): void { - if (this._isCanceled || this._hasErr) { + if (this._hasValue || this._hasErr) { return; } @@ -67,7 +61,7 @@ export class LazyPromise implements TPromise { } public resolveErr(err: any): void { - if (this._isCanceled || this._hasValue) { + if (this._hasValue || this._hasErr) { return; } @@ -84,32 +78,6 @@ export class LazyPromise implements TPromise { } public then(success: any, error: any): any { - if (this._isCanceled) { - return; - } - return this._ensureActual().then(success, error); } - - public done(success: any, error: any): void { - if (this._isCanceled) { - return; - } - - this._ensureActual().done(success, error); - } - - public cancel(): void { - if (this._hasValue || this._hasErr) { - return; - } - - this._isCanceled = true; - - if (this._actual) { - this._actual.cancel(); - } else { - this._onCancel(); - } - } } diff --git a/src/vs/workbench/services/extensions/node/proxyIdentifier.ts b/src/vs/workbench/services/extensions/node/proxyIdentifier.ts index 3447bd4255f..f088c3b1f06 100644 --- a/src/vs/workbench/services/extensions/node/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/node/proxyIdentifier.ts @@ -22,27 +22,35 @@ export interface IRPCProtocol { } export class ProxyIdentifier { + public static count = 0; _proxyIdentifierBrand: void; _suppressCompilerUnusedWarning: T; public readonly isMain: boolean; - public readonly id: string; + public readonly sid: string; + public readonly nid: number; - constructor(isMain: boolean, id: string) { + constructor(isMain: boolean, sid: string) { this.isMain = isMain; - this.id = id; + this.sid = sid; + this.nid = (++ProxyIdentifier.count); } } -/** - * Using `isFancy` indicates that arguments or results of type `URI` or `RegExp` - * will be serialized/deserialized automatically, but this has a performance cost, - * as each argument/result must be visited. - */ +const identifiers: ProxyIdentifier[] = []; + export function createMainContextProxyIdentifier(identifier: string): ProxyIdentifier { - return new ProxyIdentifier(true, 'm' + identifier); + const result = new ProxyIdentifier(true, identifier); + identifiers[result.nid] = result; + return result; } export function createExtHostContextProxyIdentifier(identifier: string): ProxyIdentifier { - return new ProxyIdentifier(false, 'e' + identifier); + const result = new ProxyIdentifier(false, identifier); + identifiers[result.nid] = result; + return result; +} + +export function getStringIdentifierForProxy(nid: number): string { + return identifiers[nid].sid; } diff --git a/src/vs/workbench/services/extensions/node/rpcProtocol.ts b/src/vs/workbench/services/extensions/node/rpcProtocol.ts index f3878f3cac8..4ad109079ba 100644 --- a/src/vs/workbench/services/extensions/node/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/node/rpcProtocol.ts @@ -6,20 +6,17 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise'; -import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; +import { ProxyIdentifier, IRPCProtocol, getStringIdentifierForProxy } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { CharCode } from 'vs/base/common/charCode'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { MarshalledObject } from 'vs/base/common/marshalling'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; declare var Proxy: any; // TODO@TypeScript -export interface IURITransformer { - transformIncoming(uri: UriComponents): UriComponents; - transformOutgoing(uri: URI): URI; -} - function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any { if (!obj || depth > 200) { @@ -45,7 +42,7 @@ function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: n return null; } -function transformOutgoingURIs(obj: any, transformer: IURITransformer): any { +export function transformOutgoingURIs(obj: any, transformer: IURITransformer): any { const result = _transformOutgoingURIs(obj, transformer, 0); if (result === null) { // no change @@ -89,26 +86,45 @@ function transformIncomingURIs(obj: any, transformer: IURITransformer): any { return result; } +export const enum RequestInitiator { + LocalSide = 0, + OtherSide = 1 +} + +export interface IRPCProtocolLogger { + logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void; + logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void; +} + +const noop = () => { }; + export class RPCProtocol implements IRPCProtocol { + private readonly _protocol: IMessagePassingProtocol; + private readonly _logger: IRPCProtocolLogger; private readonly _uriTransformer: IURITransformer; private _isDisposed: boolean; - private readonly _locals: { [id: string]: any; }; - private readonly _proxies: { [id: string]: any; }; + private readonly _locals: any[]; + private readonly _proxies: any[]; private _lastMessageId: number; - private readonly _invokedHandlers: { [req: string]: TPromise; }; + private readonly _cancelInvokedHandlers: { [req: string]: () => void; }; private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; }; - private readonly _multiplexor: RPCMultiplexer; - constructor(protocol: IMessagePassingProtocol, transformer: IURITransformer = null) { + constructor(protocol: IMessagePassingProtocol, logger: IRPCProtocolLogger = null, transformer: IURITransformer = null) { + this._protocol = protocol; + this._logger = logger; this._uriTransformer = transformer; this._isDisposed = false; - this._locals = Object.create(null); - this._proxies = Object.create(null); + this._locals = []; + this._proxies = []; + for (let i = 0, len = ProxyIdentifier.count; i < len; i++) { + this._locals[i] = null; + this._proxies[i] = null; + } this._lastMessageId = 0; - this._invokedHandlers = Object.create(null); + this._cancelInvokedHandlers = Object.create(null); this._pendingRPCReplies = {}; - this._multiplexor = new RPCMultiplexer(protocol, (msg) => this._receiveOneMessage(msg)); + this._protocol.onMessage((msg) => this._receiveOneMessage(msg)); } public dispose(): void { @@ -129,18 +145,19 @@ export class RPCProtocol implements IRPCProtocol { } public getProxy(identifier: ProxyIdentifier): T { - if (!this._proxies[identifier.id]) { - this._proxies[identifier.id] = this._createProxy(identifier.id); + const rpcId = identifier.nid; + if (!this._proxies[rpcId]) { + this._proxies[rpcId] = this._createProxy(rpcId); } - return this._proxies[identifier.id]; + return this._proxies[rpcId]; } - private _createProxy(proxyId: string): T { + private _createProxy(rpcId: number): T { let handler = { get: (target: any, name: string) => { if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) { target[name] = (...myArgs: any[]) => { - return this._remoteCall(proxyId, name, myArgs); + return this._remoteCall(rpcId, name, myArgs); }; } return target[name]; @@ -150,72 +167,140 @@ export class RPCProtocol implements IRPCProtocol { } public set(identifier: ProxyIdentifier, value: R): R { - this._locals[identifier.id] = value; + this._locals[identifier.nid] = value; return value; } public assertRegistered(identifiers: ProxyIdentifier[]): void { for (let i = 0, len = identifiers.length; i < len; i++) { const identifier = identifiers[i]; - if (!this._locals[identifier.id]) { - throw new Error(`Missing actor ${identifier.id} (isMain: ${identifier.isMain})`); + if (!this._locals[identifier.nid]) { + throw new Error(`Missing actor ${identifier.sid} (isMain: ${identifier.isMain})`); } } } - private _receiveOneMessage(rawmsg: string): void { + private _receiveOneMessage(rawmsg: Buffer): void { if (this._isDisposed) { return; } - let msg = JSON.parse(rawmsg); - if (this._uriTransformer) { - msg = transformIncomingURIs(msg, this._uriTransformer); - } + const msgLength = rawmsg.length; + const buff = MessageBuffer.read(rawmsg, 0); + const messageType = buff.readUInt8(); + const req = buff.readUInt32(); - switch (msg.type) { - case MessageType.Request: - this._receiveRequest(msg); + switch (messageType) { + case MessageType.RequestJSONArgs: + case MessageType.RequestJSONArgsWithCancellation: { + let { rpcId, method, args } = MessageIO.deserializeRequestJSONArgs(buff); + if (this._uriTransformer) { + args = transformIncomingURIs(args, this._uriTransformer); + } + this._receiveRequest(msgLength, req, rpcId, method, args, (messageType === MessageType.RequestJSONArgsWithCancellation)); break; - case MessageType.Cancel: - this._receiveCancel(msg); + } + case MessageType.RequestMixedArgs: + case MessageType.RequestMixedArgsWithCancellation: { + let { rpcId, method, args } = MessageIO.deserializeRequestMixedArgs(buff); + if (this._uriTransformer) { + args = transformIncomingURIs(args, this._uriTransformer); + } + this._receiveRequest(msgLength, req, rpcId, method, args, (messageType === MessageType.RequestMixedArgsWithCancellation)); break; - case MessageType.Reply: - this._receiveReply(msg); + } + case MessageType.Cancel: { + this._receiveCancel(msgLength, req); break; - case MessageType.ReplyErr: - this._receiveReplyErr(msg); + } + case MessageType.ReplyOKEmpty: { + this._receiveReply(msgLength, req, undefined); break; + } + case MessageType.ReplyOKJSON: { + let value = MessageIO.deserializeReplyOKJSON(buff); + if (this._uriTransformer) { + value = transformIncomingURIs(value, this._uriTransformer); + } + this._receiveReply(msgLength, req, value); + break; + } + case MessageType.ReplyOKBuffer: { + let value = MessageIO.deserializeReplyOKBuffer(buff); + this._receiveReply(msgLength, req, value); + break; + } + case MessageType.ReplyErrError: { + let err = MessageIO.deserializeReplyErrError(buff); + if (this._uriTransformer) { + err = transformIncomingURIs(err, this._uriTransformer); + } + this._receiveReplyErr(msgLength, req, err); + break; + } + case MessageType.ReplyErrEmpty: { + this._receiveReplyErr(msgLength, req, undefined); + break; + } } } - private _receiveRequest(msg: RequestMessage): void { - const callId = msg.id; - const proxyId = msg.proxyId; + private _receiveRequest(msgLength: number, req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean): void { + if (this._logger) { + this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveRequest ${getStringIdentifierForProxy(rpcId)}.${method}(`, args); + } + const callId = String(req); - this._invokedHandlers[callId] = this._invokeHandler(proxyId, msg.method, msg.args); + let promise: Thenable; + let cancel: () => void; + if (usesCancellationToken) { + const cancellationTokenSource = new CancellationTokenSource(); + args.push(cancellationTokenSource.token); + promise = this._invokeHandler(rpcId, method, args); + cancel = () => cancellationTokenSource.cancel(); + } else { + // cannot be cancelled + promise = this._invokeHandler(rpcId, method, args); + cancel = noop; + } - this._invokedHandlers[callId].then((r) => { - delete this._invokedHandlers[callId]; + this._cancelInvokedHandlers[callId] = cancel; + + promise.then((r) => { + delete this._cancelInvokedHandlers[callId]; if (this._uriTransformer) { r = transformOutgoingURIs(r, this._uriTransformer); } - this._multiplexor.send(MessageFactory.replyOK(callId, r)); + const msg = MessageIO.serializeReplyOK(req, r); + if (this._logger) { + this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `reply:`, r); + } + this._protocol.send(msg); }, (err) => { - delete this._invokedHandlers[callId]; - this._multiplexor.send(MessageFactory.replyErr(callId, err)); + delete this._cancelInvokedHandlers[callId]; + const msg = MessageIO.serializeReplyErr(req, err); + if (this._logger) { + this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `replyErr:`, err); + } + this._protocol.send(msg); }); } - private _receiveCancel(msg: CancelMessage): void { - const callId = msg.id; - if (this._invokedHandlers[callId]) { - this._invokedHandlers[callId].cancel(); + private _receiveCancel(msgLength: number, req: number): void { + if (this._logger) { + this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveCancel`); + } + const callId = String(req); + if (this._cancelInvokedHandlers[callId]) { + this._cancelInvokedHandlers[callId](); } } - private _receiveReply(msg: ReplyMessage): void { - const callId = msg.id; + private _receiveReply(msgLength: number, req: number, value: any): void { + if (this._logger) { + this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReply:`, value); + } + const callId = String(req); if (!this._pendingRPCReplies.hasOwnProperty(callId)) { return; } @@ -223,11 +308,15 @@ export class RPCProtocol implements IRPCProtocol { const pendingReply = this._pendingRPCReplies[callId]; delete this._pendingRPCReplies[callId]; - pendingReply.resolveOk(msg.res); + pendingReply.resolveOk(value); } - private _receiveReplyErr(msg: ReplyErrMessage): void { - const callId = msg.id; + private _receiveReplyErr(msgLength: number, req: number, value: any): void { + if (this._logger) { + this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReplyErr:`, value); + } + + const callId = String(req); if (!this._pendingRPCReplies.hasOwnProperty(callId)) { return; } @@ -236,144 +325,398 @@ export class RPCProtocol implements IRPCProtocol { delete this._pendingRPCReplies[callId]; let err: Error = null; - if (msg.err && msg.err.$isError) { + if (value && value.$isError) { err = new Error(); - err.name = msg.err.name; - err.message = msg.err.message; - err.stack = msg.err.stack; + err.name = value.name; + err.message = value.message; + err.stack = value.stack; } pendingReply.resolveErr(err); } - private _invokeHandler(proxyId: string, methodName: string, args: any[]): TPromise { + private _invokeHandler(rpcId: number, methodName: string, args: any[]): Thenable { try { - return TPromise.as(this._doInvokeHandler(proxyId, methodName, args)); + return TPromise.as(this._doInvokeHandler(rpcId, methodName, args)); } catch (err) { return TPromise.wrapError(err); } } - private _doInvokeHandler(proxyId: string, methodName: string, args: any[]): any { - if (!this._locals[proxyId]) { - throw new Error('Unknown actor ' + proxyId); + private _doInvokeHandler(rpcId: number, methodName: string, args: any[]): any { + const actor = this._locals[rpcId]; + if (!actor) { + throw new Error('Unknown actor ' + getStringIdentifierForProxy(rpcId)); } - let actor = this._locals[proxyId]; let method = actor[methodName]; if (typeof method !== 'function') { - throw new Error('Unknown method ' + methodName + ' on actor ' + proxyId); + throw new Error('Unknown method ' + methodName + ' on actor ' + getStringIdentifierForProxy(rpcId)); } return method.apply(actor, args); } - private _remoteCall(proxyId: string, methodName: string, args: any[]): TPromise { + private _remoteCall(rpcId: number, methodName: string, args: any[]): Thenable { if (this._isDisposed) { return TPromise.wrapError(errors.canceled()); } + let cancellationToken: CancellationToken = null; + if (args.length > 0 && CancellationToken.isCancellationToken(args[args.length - 1])) { + cancellationToken = args.pop(); + } - const callId = String(++this._lastMessageId); - const result = new LazyPromise(() => { - this._multiplexor.send(MessageFactory.cancel(callId)); - }); + if (cancellationToken && cancellationToken.isCancellationRequested) { + // No need to do anything... + return TPromise.wrapError(errors.canceled()); + } + + const req = ++this._lastMessageId; + const callId = String(req); + const result = new LazyPromise(); + + if (cancellationToken) { + cancellationToken.onCancellationRequested(() => { + const msg = MessageIO.serializeCancel(req); + if (this._logger) { + this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `cancel`); + } + this._protocol.send(MessageIO.serializeCancel(req)); + }); + } this._pendingRPCReplies[callId] = result; if (this._uriTransformer) { args = transformOutgoingURIs(args, this._uriTransformer); } - this._multiplexor.send(MessageFactory.request(callId, proxyId, methodName, args)); + const msg = MessageIO.serializeRequest(req, rpcId, methodName, args, !!cancellationToken); + if (this._logger) { + this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `request: ${getStringIdentifierForProxy(rpcId)}.${methodName}(`, args); + } + this._protocol.send(msg); return result; } } -/** - * Sends/Receives multiple messages in one go: - * - multiple messages to be sent from one stack get sent in bulk at `process.nextTick`. - * - each incoming message is handled in a separate `process.nextTick`. - */ -class RPCMultiplexer { +class MessageBuffer { - private readonly _protocol: IMessagePassingProtocol; - private readonly _sendAccumulatedBound: () => void; + public static alloc(type: MessageType, req: number, messageSize: number): MessageBuffer { + let result = new MessageBuffer(Buffer.allocUnsafe(messageSize + 1 /* type */ + 4 /* req */), 0); + result.writeUInt8(type); + result.writeUInt32(req); + return result; + } - private _messagesToSend: string[]; + public static read(buff: Buffer, offset: number): MessageBuffer { + return new MessageBuffer(buff, offset); + } - constructor(protocol: IMessagePassingProtocol, onMessage: (msg: string) => void) { - this._protocol = protocol; - this._sendAccumulatedBound = this._sendAccumulated.bind(this); + private _buff: Buffer; + private _offset: number; - this._messagesToSend = []; + public get buffer(): Buffer { + return this._buff; + } - this._protocol.onMessage(data => { - for (let i = 0, len = data.length; i < len; i++) { - onMessage(data[i]); + private constructor(buff: Buffer, offset: number) { + this._buff = buff; + this._offset = offset; + } + + public static sizeUInt8(): number { + return 1; + } + + public writeUInt8(n: number): void { + this._buff.writeUInt8(n, this._offset, true); this._offset += 1; + } + + public readUInt8(): number { + const n = this._buff.readUInt8(this._offset, true); this._offset += 1; + return n; + } + + public writeUInt32(n: number): void { + this._buff.writeUInt32BE(n, this._offset, true); this._offset += 4; + } + + public readUInt32(): number { + const n = this._buff.readUInt32BE(this._offset, true); this._offset += 4; + return n; + } + + public static sizeShortString(str: string, strByteLength: number): number { + return 1 /* string length */ + strByteLength /* actual string */; + } + + public writeShortString(str: string, strByteLength: number): void { + this._buff.writeUInt8(strByteLength, this._offset, true); this._offset += 1; + this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength; + } + + public readShortString(): string { + const strLength = this._buff.readUInt8(this._offset, true); this._offset += 1; + const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength; + return str; + } + + public static sizeLongString(str: string, strByteLength: number): number { + return 4 /* string length */ + strByteLength /* actual string */; + } + + public writeLongString(str: string, strByteLength: number): void { + this._buff.writeUInt32LE(strByteLength, this._offset, true); this._offset += 4; + this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength; + } + + public readLongString(): string { + const strLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4; + const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength; + return str; + } + + public static sizeBuffer(buff: Buffer, buffByteLength: number): number { + return 4 /* buffer length */ + buffByteLength /* actual buffer */; + } + + public writeBuffer(buff: Buffer, buffByteLength: number): void { + this._buff.writeUInt32LE(buffByteLength, this._offset, true); this._offset += 4; + buff.copy(this._buff, this._offset); this._offset += buffByteLength; + } + + public readBuffer(): Buffer { + const buffLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4; + const buff = this._buff.slice(this._offset, this._offset + buffLength); this._offset += buffLength; + return buff; + } + + public static sizeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): number { + let size = 0; + size += 1; // arr length + for (let i = 0, len = arr.length; i < len; i++) { + const el = arr[i]; + const elLength = arrLengths[i]; + size += 1; // arg type + if (typeof el === 'string') { + size += this.sizeLongString(el, elLength); + } else { + size += this.sizeBuffer(el, elLength); } - }); - } - - private _sendAccumulated(): void { - const tmp = this._messagesToSend; - this._messagesToSend = []; - this._protocol.send(tmp); - } - - public send(msg: string): void { - if (this._messagesToSend.length === 0) { - process.nextTick(this._sendAccumulatedBound); } - this._messagesToSend.push(msg); + return size; + } + + public writeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): void { + this._buff.writeUInt8(arr.length, this._offset, true); this._offset += 1; + for (let i = 0, len = arr.length; i < len; i++) { + const el = arr[i]; + const elLength = arrLengths[i]; + if (typeof el === 'string') { + this.writeUInt8(ArgType.String); + this.writeLongString(el, elLength); + } else { + this.writeUInt8(ArgType.Buffer); + this.writeBuffer(el, elLength); + } + } + } + + public readMixedArray(): (string | Buffer)[] { + const arrLen = this._buff.readUInt8(this._offset, true); this._offset += 1; + let arr: (string | Buffer)[] = new Array(arrLen); + for (let i = 0; i < arrLen; i++) { + const argType = this.readUInt8(); + if (argType === ArgType.String) { + arr[i] = this.readLongString(); + } else { + arr[i] = this.readBuffer(); + } + } + return arr; } } -class MessageFactory { - public static cancel(req: string): string { - return `{"type":${MessageType.Cancel},"id":"${req}"}`; +class MessageIO { + + private static _arrayContainsBuffer(arr: any[]): boolean { + for (let i = 0, len = arr.length; i < len; i++) { + if (Buffer.isBuffer(arr[i])) { + return true; + } + } + return false; } - public static request(req: string, rpcId: string, method: string, args: any[]): string { - return `{"type":${MessageType.Request},"id":"${req}","proxyId":"${rpcId}","method":"${method}","args":${JSON.stringify(args)}}`; + public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean): Buffer { + if (this._arrayContainsBuffer(args)) { + let massagedArgs: (string | Buffer)[] = new Array(args.length); + let argsLengths: number[] = new Array(args.length); + for (let i = 0, len = args.length; i < len; i++) { + const arg = args[i]; + if (Buffer.isBuffer(arg)) { + massagedArgs[i] = arg; + argsLengths[i] = arg.byteLength; + } else { + massagedArgs[i] = JSON.stringify(arg); + argsLengths[i] = Buffer.byteLength(massagedArgs[i], 'utf8'); + } + } + return this._requestMixedArgs(req, rpcId, method, massagedArgs, argsLengths, usesCancellationToken); + } + return this._requestJSONArgs(req, rpcId, method, JSON.stringify(args), usesCancellationToken); } - public static replyOK(req: string, res: any): string { + private static _requestJSONArgs(req: number, rpcId: number, method: string, args: string, usesCancellationToken: boolean): Buffer { + const methodByteLength = Buffer.byteLength(method, 'utf8'); + const argsByteLength = Buffer.byteLength(args, 'utf8'); + + let len = 0; + len += MessageBuffer.sizeUInt8(); + len += MessageBuffer.sizeShortString(method, methodByteLength); + len += MessageBuffer.sizeLongString(args, argsByteLength); + + let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestJSONArgsWithCancellation : MessageType.RequestJSONArgs, req, len); + result.writeUInt8(rpcId); + result.writeShortString(method, methodByteLength); + result.writeLongString(args, argsByteLength); + return result.buffer; + } + + public static deserializeRequestJSONArgs(buff: MessageBuffer): { rpcId: number; method: string; args: any[]; } { + const rpcId = buff.readUInt8(); + const method = buff.readShortString(); + const args = buff.readLongString(); + return { + rpcId: rpcId, + method: method, + args: JSON.parse(args) + }; + } + + private static _requestMixedArgs(req: number, rpcId: number, method: string, args: (string | Buffer)[], argsLengths: number[], usesCancellationToken: boolean): Buffer { + const methodByteLength = Buffer.byteLength(method, 'utf8'); + + let len = 0; + len += MessageBuffer.sizeUInt8(); + len += MessageBuffer.sizeShortString(method, methodByteLength); + len += MessageBuffer.sizeMixedArray(args, argsLengths); + + let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestMixedArgsWithCancellation : MessageType.RequestMixedArgs, req, len); + result.writeUInt8(rpcId); + result.writeShortString(method, methodByteLength); + result.writeMixedArray(args, argsLengths); + return result.buffer; + } + + public static deserializeRequestMixedArgs(buff: MessageBuffer): { rpcId: number; method: string; args: any[]; } { + const rpcId = buff.readUInt8(); + const method = buff.readShortString(); + const rawargs = buff.readMixedArray(); + const args: any[] = new Array(rawargs.length); + for (let i = 0, len = rawargs.length; i < len; i++) { + const rawarg = rawargs[i]; + if (typeof rawarg === 'string') { + args[i] = JSON.parse(rawarg); + } else { + args[i] = rawarg; + } + } + return { + rpcId: rpcId, + method: method, + args: args + }; + } + + public static serializeCancel(req: number): Buffer { + return MessageBuffer.alloc(MessageType.Cancel, req, 0).buffer; + } + + public static serializeReplyOK(req: number, res: any): Buffer { if (typeof res === 'undefined') { - return `{"type":${MessageType.Reply},"id":"${req}"}`; + return this._serializeReplyOKEmpty(req); } - return `{"type":${MessageType.Reply},"id":"${req}","res":${JSON.stringify(res)}}`; + if (Buffer.isBuffer(res)) { + return this._serializeReplyOKBuffer(req, res); + } + return this._serializeReplyOKJSON(req, JSON.stringify(res)); } - public static replyErr(req: string, err: any): string { + private static _serializeReplyOKEmpty(req: number): Buffer { + return MessageBuffer.alloc(MessageType.ReplyOKEmpty, req, 0).buffer; + } + + private static _serializeReplyOKBuffer(req: number, res: Buffer): Buffer { + const resByteLength = res.byteLength; + + let len = 0; + len += MessageBuffer.sizeBuffer(res, resByteLength); + + let result = MessageBuffer.alloc(MessageType.ReplyOKBuffer, req, len); + result.writeBuffer(res, resByteLength); + return result.buffer; + } + + public static deserializeReplyOKBuffer(buff: MessageBuffer): Buffer { + return buff.readBuffer(); + } + + private static _serializeReplyOKJSON(req: number, res: string): Buffer { + const resByteLength = Buffer.byteLength(res, 'utf8'); + + let len = 0; + len += MessageBuffer.sizeLongString(res, resByteLength); + + let result = MessageBuffer.alloc(MessageType.ReplyOKJSON, req, len); + result.writeLongString(res, resByteLength); + return result.buffer; + } + + public static deserializeReplyOKJSON(buff: MessageBuffer): any { + const res = buff.readLongString(); + return JSON.parse(res); + } + + public static serializeReplyErr(req: number, err: any): Buffer { if (err instanceof Error) { - return `{"type":${MessageType.ReplyErr},"id":"${req}","err":${JSON.stringify(errors.transformErrorForSerialization(err))}}`; + return this._serializeReplyErrEror(req, err); } - return `{"type":${MessageType.ReplyErr},"id":"${req}","err":null}`; + return this._serializeReplyErrEmpty(req); + } + + private static _serializeReplyErrEror(req: number, _err: Error): Buffer { + const err = JSON.stringify(errors.transformErrorForSerialization(_err)); + const errByteLength = Buffer.byteLength(err, 'utf8'); + + let len = 0; + len += MessageBuffer.sizeLongString(err, errByteLength); + + let result = MessageBuffer.alloc(MessageType.ReplyErrError, req, len); + result.writeLongString(err, errByteLength); + return result.buffer; + } + + public static deserializeReplyErrError(buff: MessageBuffer): Error { + const err = buff.readLongString(); + return JSON.parse(err); + } + + private static _serializeReplyErrEmpty(req: number): Buffer { + return MessageBuffer.alloc(MessageType.ReplyErrEmpty, req, 0).buffer; } } const enum MessageType { - Request = 1, - Cancel = 2, - Reply = 3, - ReplyErr = 4 + RequestJSONArgs = 1, + RequestJSONArgsWithCancellation = 2, + RequestMixedArgs = 3, + RequestMixedArgsWithCancellation = 4, + Cancel = 5, + ReplyOKEmpty = 6, + ReplyOKBuffer = 7, + ReplyOKJSON = 8, + ReplyErrError = 9, + ReplyErrEmpty = 10, } -class RequestMessage { - type: MessageType.Request; - id: string; - proxyId: string; - method: string; - args: any[]; +const enum ArgType { + String = 1, + Buffer = 2 } -class CancelMessage { - type: MessageType.Cancel; - id: string; -} -class ReplyMessage { - type: MessageType.Reply; - id: string; - res: any; -} -class ReplyErrMessage { - type: MessageType.ReplyErr; - id: string; - err: errors.SerializedError; -} - -type RPCMessage = RequestMessage | CancelMessage | ReplyMessage | ReplyErrMessage; diff --git a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts new file mode 100644 index 00000000000..e6156147f3c --- /dev/null +++ b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; + +suite('RPCProtocol', () => { + + class MessagePassingProtocol implements IMessagePassingProtocol { + private _pair: MessagePassingProtocol; + + private readonly _onMessage: Emitter = new Emitter(); + public readonly onMessage: Event = this._onMessage.event; + + public setPair(other: MessagePassingProtocol) { + this._pair = other; + } + + public send(buffer: Buffer): void { + process.nextTick(() => { + this._pair._onMessage.fire(buffer); + }); + } + } + + let delegate: (a1: any, a2: any) => any; + let bProxy: BClass; + class BClass { + $m(a1: any, a2: any): Thenable { + return TPromise.as(delegate.call(null, a1, a2)); + } + } + + setup(() => { + let a_protocol = new MessagePassingProtocol(); + let b_protocol = new MessagePassingProtocol(); + a_protocol.setPair(b_protocol); + b_protocol.setPair(a_protocol); + + let A = new RPCProtocol(a_protocol); + let B = new RPCProtocol(b_protocol); + + delegate = null; + + const bIdentifier = new ProxyIdentifier(false, 'bb'); + const bInstance = new BClass(); + B.set(bIdentifier, bInstance); + bProxy = A.getProxy(bIdentifier); + }); + + test('simple call', function (done) { + delegate = (a1: number, a2: number) => a1 + a2; + bProxy.$m(4, 1).then((res: number) => { + assert.equal(res, 5); + done(null); + }, done); + }); + + test('simple call without result', function (done) { + delegate = (a1: number, a2: number) => { }; + bProxy.$m(4, 1).then((res: number) => { + assert.equal(res, undefined); + done(null); + }, done); + }); + + test('passing buffer as argument', function (done) { + delegate = (a1: Buffer, a2: number) => { + assert.ok(Buffer.isBuffer(a1)); + return a1[a2]; + }; + let b = Buffer.allocUnsafe(4); + b[0] = 1; + b[1] = 2; + b[2] = 3; + b[3] = 4; + bProxy.$m(b, 2).then((res: number) => { + assert.equal(res, 3); + done(null); + }, done); + }); + + test('returning a buffer', function (done) { + delegate = (a1: number, a2: number) => { + let b = Buffer.allocUnsafe(4); + b[0] = 1; + b[1] = 2; + b[2] = 3; + b[3] = 4; + return b; + }; + bProxy.$m(4, 1).then((res: Buffer) => { + assert.ok(Buffer.isBuffer(res)); + assert.equal(res[0], 1); + assert.equal(res[1], 2); + assert.equal(res[2], 3); + assert.equal(res[3], 4); + done(null); + }, done); + }); + + test('cancelling a call via CancellationToken before', function (done) { + delegate = (a1: number, a2: number) => a1 + a2; + let p = bProxy.$m(4, CancellationToken.Cancelled); + p.then((res: number) => { + assert.fail('should not receive result'); + }, (err) => { + assert.ok(true); + done(null); + }); + }); + + test('passing CancellationToken.None', function (done) { + delegate = (a1: number, token: CancellationToken) => { + assert.ok(!!token); + return a1 + 1; + }; + bProxy.$m(4, CancellationToken.None).then((res: number) => { + assert.equal(res, 5); + done(null); + }, done); + }); + + test('cancelling a call via CancellationToken quickly', function (done) { + // this is an implementation which, when cancellation is triggered, will return 7 + delegate = (a1: number, token: CancellationToken) => { + return new TPromise((resolve, reject) => { + token.onCancellationRequested((e) => { + resolve(7); + }); + }); + }; + let tokenSource = new CancellationTokenSource(); + let p = bProxy.$m(4, tokenSource.token); + p.then((res: number) => { + assert.equal(res, 7); + done(null); + }, (err) => { + assert.fail('should not receive error'); + done(); + }); + tokenSource.cancel(); + }); + + test('throwing an error', function (done) { + delegate = (a1: number, a2: number) => { + throw new Error(`nope`); + }; + bProxy.$m(4, 1).then((res) => { + assert.fail('unexpected'); + done(null); + }, (err) => { + assert.equal(err.message, 'nope'); + done(null); + }); + }); + + test('error promise', function (done) { + delegate = (a1: number, a2: number) => { + return TPromise.wrapError(undefined); + }; + bProxy.$m(4, 1).then((res) => { + assert.fail('unexpected'); + done(null); + }, (err) => { + assert.equal(err, undefined); + done(null); + }); + }); +}); diff --git a/src/vs/workbench/services/files/electron-browser/encoding.ts b/src/vs/workbench/services/files/electron-browser/encoding.ts index c1b99f40985..b85375e7665 100644 --- a/src/vs/workbench/services/files/electron-browser/encoding.ts +++ b/src/vs/workbench/services/files/electron-browser/encoding.ts @@ -7,14 +7,14 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import * as encoding from 'vs/base/node/encoding'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { IResolveContentOptions, isParent, IResourceEncodings } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { join, extname } from 'path'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface IEncodingOverride { parent?: uri; @@ -26,9 +26,8 @@ export interface IEncodingOverride { // service and then ideally be passed in as option to the file service // the file service should talk about string | Buffer for reading and writing and only convert // to strings if a encoding is provided -export class ResourceEncodings implements IResourceEncodings { +export class ResourceEncodings extends Disposable implements IResourceEncodings { private encodingOverride: IEncodingOverride[]; - private toDispose: IDisposable[]; constructor( private textResourceConfigurationService: ITextResourceConfigurationService, @@ -36,8 +35,9 @@ export class ResourceEncodings implements IResourceEncodings { private contextService: IWorkspaceContextService, encodingOverride?: IEncodingOverride[] ) { + super(); + this.encodingOverride = encodingOverride || this.getEncodingOverrides(); - this.toDispose = []; this.registerListeners(); } @@ -45,12 +45,12 @@ export class ResourceEncodings implements IResourceEncodings { private registerListeners(): void { // Workspace Folder Change - this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => { + this._register(this.contextService.onDidChangeWorkspaceFolders(() => { this.encodingOverride = this.getEncodingOverrides(); })); } - public getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string { + getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string { let preferredEncoding: string; // Encoding passed in as option @@ -79,7 +79,7 @@ export class ResourceEncodings implements IResourceEncodings { return this.getEncodingForResource(resource, preferredEncoding); } - public getWriteEncoding(resource: uri, preferredEncoding?: string): string { + getWriteEncoding(resource: uri, preferredEncoding?: string): string { return this.getEncodingForResource(resource, preferredEncoding); } @@ -138,8 +138,4 @@ export class ResourceEncodings implements IResourceEncodings { return null; } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index cacc1dc7a68..2c834e3e1ac 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -18,11 +18,11 @@ import * as arrays from 'vs/base/common/arrays'; import { TPromise } from 'vs/base/common/winjs.base'; import * as objects from 'vs/base/common/objects'; import * as extfs from 'vs/base/node/extfs'; -import { nfcall, ThrottledDelayer, asWinJSImport } from 'vs/base/common/async'; -import uri from 'vs/base/common/uri'; +import { nfcall, ThrottledDelayer } from 'vs/base/common/async'; +import { URI as uri } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import * as pfs from 'vs/base/node/pfs'; import * as encoding from 'vs/base/node/encoding'; @@ -76,9 +76,9 @@ export interface IFileServiceTestOptions { encodingOverride?: IEncodingOverride[]; } -export class FileService implements IFileService { +export class FileService extends Disposable implements IFileService { - public _serviceBrand: any; + _serviceBrand: any; private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) @@ -89,14 +89,17 @@ export class FileService implements IFileService { private static readonly ENOSPC_ERROR = 'ENOSPC'; private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError'; - protected readonly _onFileChanges: Emitter; - protected readonly _onAfterOperation: Emitter; - protected readonly _onDidChangeFileSystemProviderRegistrations = new Emitter(); + protected readonly _onFileChanges: Emitter = this._register(new Emitter()); + get onFileChanges(): Event { return this._onFileChanges.event; } - protected toDispose: IDisposable[]; + protected readonly _onAfterOperation: Emitter = this._register(new Emitter()); + get onAfterOperation(): Event { return this._onAfterOperation.event; } + + protected readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter()); + get onDidChangeFileSystemProviderRegistrations(): Event { return this._onDidChangeFileSystemProviderRegistrations.event; } private activeWorkspaceFileChangeWatcher: IDisposable; - private activeFileChangesWatchers: ResourceMap; + private activeFileChangesWatchers: ResourceMap<{ unwatch: Function, count: number }>; private fileChangesWatchDelayer: ThrottledDelayer; private undeliveredRawFileChangesEvents: IRawFileChange[]; @@ -112,15 +115,9 @@ export class FileService implements IFileService { private notificationService: INotificationService, private options: IFileServiceTestOptions = Object.create(null) ) { - this.toDispose = []; + super(); - this._onFileChanges = new Emitter(); - this.toDispose.push(this._onFileChanges); - - this._onAfterOperation = new Emitter(); - this.toDispose.push(this._onAfterOperation); - - this.activeFileChangesWatchers = new ResourceMap(); + this.activeFileChangesWatchers = new ResourceMap<{ unwatch: Function, count: number }>(); this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); this.undeliveredRawFileChangesEvents = []; @@ -129,7 +126,7 @@ export class FileService implements IFileService { this.registerListeners(); } - public get encoding(): ResourceEncodings { + get encoding(): ResourceEncodings { return this._encoding; } @@ -141,7 +138,7 @@ export class FileService implements IFileService { }); // Workbench State Change - this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => { + this._register(this.contextService.onDidChangeWorkbenchState(() => { if (this.lifecycleService.phase >= LifecyclePhase.Running) { this.setupFileWatching(); } @@ -195,14 +192,6 @@ export class FileService implements IFileService { } } - public get onFileChanges(): Event { - return this._onFileChanges.event; - } - - public get onAfterOperation(): Event { - return this._onAfterOperation.event; - } - private setupFileWatching(): void { // dispose old if any @@ -240,30 +229,28 @@ export class FileService implements IFileService { } } - public readonly onDidChangeFileSystemProviderRegistrations: Event = this._onDidChangeFileSystemProviderRegistrations.event; - - public registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { throw new Error('not implemented'); } - public canHandleResource(resource: uri): boolean { + canHandleResource(resource: uri): boolean { return resource.scheme === Schemas.file; } - public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { + resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { return this.resolve(resource, options); } - public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { + resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options) .then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false })))); } - public existsFile(resource: uri): TPromise { + existsFile(resource: uri): TPromise { return this.resolveFile(resource).then(() => true, () => false); } - public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { + resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { return this.resolveStreamContent(resource, options).then(streamContent => { return new TPromise((resolve, reject) => { @@ -273,6 +260,7 @@ export class FileService implements IFileService { mtime: streamContent.mtime, etag: streamContent.etag, encoding: streamContent.encoding, + isReadonly: streamContent.isReadonly, value: '' }; @@ -285,7 +273,7 @@ export class FileService implements IFileService { }); } - public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { + resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { // Guard early against attempts to resolve an invalid file path if (resource.scheme !== Schemas.file || !resource.fsPath) { @@ -302,6 +290,7 @@ export class FileService implements IFileService { mtime: void 0, etag: void 0, encoding: void 0, + isReadonly: false, value: void 0 }; @@ -575,7 +564,7 @@ export class FileService implements IFileService { }); } - public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { if (options.writeElevated) { return this.doUpdateContentElevated(resource, value, options); } @@ -618,22 +607,23 @@ export class FileService implements IFileService { return addBomPromise.then(addBom => { // 4.) set contents and resolve - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite).then(void 0, error => { - if (!exists || error.code !== 'EPERM' || !isWindows) { - return TPromise.wrapError(error); - } + if (!exists || !isWindows) { + return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite); + } - // On Windows and if the file exists with an EPERM error, we try a different strategy of saving the file - // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows - // (see https://github.com/Microsoft/vscode/issues/931) + // On Windows and if the file exists, we use a different strategy of saving the file + // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams + // (see https://github.com/Microsoft/vscode/issues/6363) + else { - // 5.) truncate + // 4.) truncate return pfs.truncate(absolutePath, 0).then(() => { - // 6.) set contents (this time with r+ mode) and resolve again + // 5.) set contents (with r+ mode) and resolve return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }); }); - }); + } }); }); }).then(null, error => { @@ -689,7 +679,7 @@ export class FileService implements IFileService { return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => { // 3.) invoke our CLI as super user - return asWinJSImport(import('sudo-prompt')).then(sudoPrompt => { + return TPromise.wrap(import('sudo-prompt')).then(sudoPrompt => { return new TPromise((c, e) => { const promptOptions = { name: this.environmentService.appNameLong.replace('-', ''), @@ -737,7 +727,7 @@ export class FileService implements IFileService { }); } - public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { + createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); let checkFilePromise: TPromise; @@ -768,7 +758,7 @@ export class FileService implements IFileService { }); } - public createFolder(resource: uri): TPromise { + createFolder(resource: uri): TPromise { // 1.) Create folder const absolutePath = this.toAbsolutePath(resource); @@ -847,17 +837,11 @@ export class FileService implements IFileService { )); } - public rename(resource: uri, newName: string): TPromise { - const newPath = paths.join(paths.dirname(resource.fsPath), newName); - - return this.moveFile(resource, uri.file(newPath)); - } - - public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { + moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { return this.moveOrCopyFile(source, target, false, overwrite); } - public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { + copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { return this.moveOrCopyFile(source, target, true, overwrite); } @@ -903,7 +887,7 @@ export class FileService implements IFileService { return TPromise.wrapError(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! } - deleteTargetPromise = this.del(uri.file(targetPath)); + deleteTargetPromise = this.del(uri.file(targetPath), { recursive: true }); } return deleteTargetPromise.then(() => { @@ -924,36 +908,56 @@ export class FileService implements IFileService { }); } - public del(resource: uri, useTrash?: boolean): TPromise { - if (useTrash) { + del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): TPromise { + if (options && options.useTrash) { return this.doMoveItemToTrash(resource); } - return this.doDelete(resource); + return this.doDelete(resource, options && options.recursive); } private doMoveItemToTrash(resource: uri): TPromise { const absolutePath = resource.fsPath; - return asWinJSImport(import('electron')).then(electron => { - const result = electron.shell.moveItemToTrash(absolutePath); - if (!result) { - return TPromise.wrapError(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath)))); - } + const shell = (require('electron') as Electron.RendererInterface).shell; // workaround for being able to run tests out of VSCode debugger + const result = shell.moveItemToTrash(absolutePath); + if (!result) { + return TPromise.wrapError(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath)))); + } - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - return void 0; - }); + return TPromise.as(void 0); } - private doDelete(resource: uri): TPromise { + private doDelete(resource: uri, recursive: boolean): TPromise { const absolutePath = this.toAbsolutePath(resource); - return pfs.del(absolutePath, os.tmpdir()).then(() => { + let assertNonRecursiveDelete: TPromise; + if (!recursive) { + assertNonRecursiveDelete = pfs.stat(absolutePath).then(stat => { + if (!stat.isDirectory()) { + return TPromise.as(void 0); + } - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + return pfs.readdir(absolutePath).then(children => { + if (children.length === 0) { + return TPromise.as(void 0); + } + + return TPromise.wrapError(new Error(nls.localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", paths.basename(absolutePath)))); + }); + }, error => TPromise.as(void 0) /* ignore errors */); + } else { + assertNonRecursiveDelete = TPromise.as(void 0); + } + + return assertNonRecursiveDelete.then(() => { + return pfs.del(absolutePath, os.tmpdir()).then(() => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + }); }); } @@ -984,62 +988,70 @@ export class FileService implements IFileService { }); } - public watchFileChanges(resource: uri): void { + watchFileChanges(resource: uri): void { assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); - // Create or get watcher for provided path - let watcher = this.activeFileChangesWatchers.get(resource); - if (!watcher) { - const fsPath = resource.fsPath; - const fsName = paths.basename(resource.fsPath); + // Check for existing watcher first + const entry = this.activeFileChangesWatchers.get(resource); + if (entry) { + entry.count += 1; - watcher = extfs.watch(fsPath, (eventType: string, filename: string) => { - const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); - - // The file was either deleted or renamed. Many tools apply changes to files in an - // atomic way ("Atomic Save") by first renaming the file to a temporary name and then - // renaming it back to the original name. Our watcher will detect this as a rename - // and then stops to work on Mac and Linux because the watcher is applied to the - // inode and not the name. The fix is to detect this case and trying to watch the file - // again after a certain delay. - // In addition, we send out a delete event if after a timeout we detect that the file - // does indeed not exist anymore. - if (renamedOrDeleted) { - - // Very important to dispose the watcher which now points to a stale inode - this.unwatchFileChanges(resource); - - // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") - setTimeout(() => { - this.existsFile(resource).done(exists => { - - // File still exists, so reapply the watcher - if (exists) { - this.watchFileChanges(resource); - } - - // File seems to be really gone, so emit a deleted event - else { - this.onRawFileChange({ - type: FileChangeType.DELETED, - path: fsPath - }); - } - }); - }, FileService.FS_REWATCH_DELAY); - } - - // Handle raw file change - this.onRawFileChange({ - type: FileChangeType.UPDATED, - path: fsPath - }); - }, (error: string) => this.handleError(error)); - - if (watcher) { - this.activeFileChangesWatchers.set(resource, watcher); - } + return; } + + // Create or get watcher for provided path + const fsPath = resource.fsPath; + const fsName = paths.basename(resource.fsPath); + + const watcherDisposable = extfs.watch(fsPath, (eventType: string, filename: string) => { + const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); + + // The file was either deleted or renamed. Many tools apply changes to files in an + // atomic way ("Atomic Save") by first renaming the file to a temporary name and then + // renaming it back to the original name. Our watcher will detect this as a rename + // and then stops to work on Mac and Linux because the watcher is applied to the + // inode and not the name. The fix is to detect this case and trying to watch the file + // again after a certain delay. + // In addition, we send out a delete event if after a timeout we detect that the file + // does indeed not exist anymore. + if (renamedOrDeleted) { + + // Very important to dispose the watcher which now points to a stale inode + watcherDisposable.dispose(); + this.activeFileChangesWatchers.delete(resource); + + // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") + setTimeout(() => { + this.existsFile(resource).then(exists => { + + // File still exists, so reapply the watcher + if (exists) { + this.watchFileChanges(resource); + } + + // File seems to be really gone, so emit a deleted event + else { + this.onRawFileChange({ + type: FileChangeType.DELETED, + path: fsPath + }); + } + }); + }, FileService.FS_REWATCH_DELAY); + } + + // Handle raw file change + this.onRawFileChange({ + type: FileChangeType.UPDATED, + path: fsPath + }); + }, (error: string) => this.handleError(error)); + + // Remember in map + this.activeFileChangesWatchers.set(resource, { + count: 1, + unwatch: () => watcherDisposable.dispose() + }); } private onRawFileChange(event: IRawFileChange): void { @@ -1048,7 +1060,7 @@ export class FileService implements IFileService { this.undeliveredRawFileChangesEvents.push(event); if (this.environmentService.verbose) { - console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); + console.log('%c[File Watcher (node.js)]%c', 'color: blue', 'color: black', `${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); } // handle emit through delayer to accommodate for bulk changes @@ -1062,7 +1074,7 @@ export class FileService implements IFileService { // Logging if (this.environmentService.verbose) { normalizedEvents.forEach(r => { - console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); + console.log('%c[File Watcher (node.js)]%c >> normalized', 'color: blue', 'color: black', `${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`); }); } @@ -1073,23 +1085,23 @@ export class FileService implements IFileService { }); } - public unwatchFileChanges(resource: uri): void { + unwatchFileChanges(resource: uri): void { const watcher = this.activeFileChangesWatchers.get(resource); - if (watcher) { - watcher.close(); + if (watcher && --watcher.count === 0) { + watcher.unwatch(); this.activeFileChangesWatchers.delete(resource); } } - public dispose(): void { - this.toDispose = dispose(this.toDispose); + dispose(): void { + super.dispose(); if (this.activeWorkspaceFileChangeWatcher) { this.activeWorkspaceFileChangeWatcher.dispose(); this.activeWorkspaceFileChangeWatcher = null; } - this.activeFileChangesWatchers.forEach(watcher => watcher.close()); + this.activeFileChangesWatchers.forEach(watcher => watcher.unwatch()); this.activeFileChangesWatchers.clear(); } } @@ -1128,13 +1140,14 @@ export class StatResolver { this.etag = etag(size, mtime); } - public resolve(options: IResolveFileOptions): TPromise { + resolve(options: IResolveFileOptions): TPromise { // General Data const fileStat: IFileStat = { resource: this.resource, isDirectory: this.isDirectory, isSymbolicLink: this.isSymbolicLink, + isReadonly: false, name: this.name, etag: this.etag, size: this.size, @@ -1219,6 +1232,7 @@ export class StatResolver { resource: fileResource, isDirectory: fileStat.isDirectory(), isSymbolicLink, + isReadonly: false, name: file, mtime: fileStat.mtime.getTime(), etag: etag(fileStat), diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index a79a41a04f9..99a00594348 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { posix } from 'path'; import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TernarySearchTree, keys } from 'vs/base/common/map'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { TernarySearchTree } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; -import URI from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; @@ -38,13 +38,14 @@ class TypeOnlyStat implements IStat { size: number = 0; } -function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): TPromise { +function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): Thenable { const [resource, stat] = tuple; const fileStat: IFileStat = { resource, - name: posix.basename(resource.path), + name: resources.basename(resource), isDirectory: (stat.type & FileType.Directory) !== 0, isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, + isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly), mtime: stat.mtime, size: stat.size, etag: stat.mtime.toString(29) + stat.size.toString(31), @@ -57,7 +58,7 @@ function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse // resolve children if requested return TPromise.join(entries.map(tuple => { const [name, type] = tuple; - const childResource = resource.with({ path: posix.join(resource.path, name) }); + const childResource = resources.joinPath(resource, name); return toIFileStat(provider, [childResource, new TypeOnlyStat(type)], recurse); })).then(children => { fileStat.children = children; @@ -71,7 +72,7 @@ function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse return TPromise.as(fileStat); } -export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to: URI[]): TPromise { +export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to: URI[]): Thenable { const trie = TernarySearchTree.forPaths(); trie.set(tuple[0].toString(), true); @@ -85,9 +86,8 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta }); } -class WorkspaceWatchLogic { +class WorkspaceWatchLogic extends Disposable { - private _disposables: IDisposable[] = []; private _watches = new Map(); constructor( @@ -95,9 +95,11 @@ class WorkspaceWatchLogic { @IConfigurationService private _configurationService: IConfigurationService, @IWorkspaceContextService private _contextService: IWorkspaceContextService, ) { + super(); + this._refresh(); - this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(e => { + this._register(this._contextService.onDidChangeWorkspaceFolders(e => { for (const removed of e.removed) { this._unwatchWorkspace(removed.uri); } @@ -105,10 +107,10 @@ class WorkspaceWatchLogic { this._watchWorkspace(added.uri); } })); - this._disposables.push(this._contextService.onDidChangeWorkbenchState(e => { + this._register(this._contextService.onDidChangeWorkbenchState(e => { this._refresh(); })); - this._disposables.push(this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('files.watcherExclude')) { this._refresh(); } @@ -117,7 +119,7 @@ class WorkspaceWatchLogic { dispose(): void { this._unwatchWorkspaces(); - this._disposables = dispose(this._disposables); + super.dispose(); } private _refresh(): void { @@ -159,12 +161,11 @@ class WorkspaceWatchLogic { export class RemoteFileService extends FileService { private readonly _provider: Map; - private readonly _lastKnownSchemes: string[]; constructor( @IExtensionService private readonly _extensionService: IExtensionService, - @IStorageService private readonly _storageService: IStorageService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, + @IEnvironmentService environmentService: IEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService contextService: IWorkspaceContextService, @ILifecycleService lifecycleService: ILifecycleService, @@ -173,17 +174,16 @@ export class RemoteFileService extends FileService { ) { super( contextService, - _environmentService, + environmentService, textResourceConfigurationService, configurationService, lifecycleService, - _storageService, + storageService, notificationService ); this._provider = new Map(); - this._lastKnownSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); - this.toDispose.push(new WorkspaceWatchLogic(this, configurationService, contextService)); + this._register(new WorkspaceWatchLogic(this, configurationService, contextService)); } registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { @@ -193,7 +193,6 @@ export class RemoteFileService extends FileService { this._provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); - this._storageService.store('remote_schemes', JSON.stringify(keys(this._provider))); const reg = provider.onDidChangeFile(changes => { // forward change events @@ -209,19 +208,7 @@ export class RemoteFileService extends FileService { } canHandleResource(resource: URI): boolean { - if (resource.scheme === Schemas.file || this._provider.has(resource.scheme)) { - return true; - } - // TODO@remote - // this needs to go, but this already went viral - // https://github.com/Microsoft/vscode/issues/48275 - if (this._lastKnownSchemes.indexOf(resource.scheme) < 0) { - return false; - } - if (!this._environmentService.isBuilt) { - console.warn('[remote] cache information required for ' + resource.toString()); - } - return true; + return resource.scheme === Schemas.file || this._provider.has(resource.scheme); } private _tryParseFileOperationResult(err: any): FileOperationResult { @@ -259,7 +246,7 @@ export class RemoteFileService extends FileService { private _withProvider(resource: URI): TPromise { - if (!posix.isAbsolute(resource.path)) { + if (!resources.isAbsolutePath(resource)) { throw new FileOperationError( localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)), FileOperationResult.FILE_INVALID_PATH @@ -280,7 +267,7 @@ export class RemoteFileService extends FileService { }); } - existsFile(resource: URI): TPromise { + existsFile(resource: URI): TPromise { if (resource.scheme === Schemas.file) { return super.existsFile(resource); } else { @@ -288,7 +275,7 @@ export class RemoteFileService extends FileService { } } - resolveFile(resource: URI, options?: IResolveFileOptions): TPromise { + resolveFile(resource: URI, options?: IResolveFileOptions): TPromise { if (resource.scheme === Schemas.file) { return super.resolveFile(resource, options); } else { @@ -305,7 +292,7 @@ export class RemoteFileService extends FileService { } } - resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { // soft-groupBy, keep order, don't rearrange/merge groups let groups: (typeof toResolve)[] = []; @@ -318,7 +305,7 @@ export class RemoteFileService extends FileService { group.push(request); } - const promises: TPromise[] = []; + const promises: TPromise[] = []; for (const group of groups) { if (group[0].resource.scheme === Schemas.file) { promises.push(super.resolveFiles(group)); @@ -329,7 +316,7 @@ export class RemoteFileService extends FileService { return TPromise.join(promises).then(data => flatten(data)); } - private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { return this._withProvider(toResolve[0].resource).then(provider => { let result: IResolveFileResult[] = []; let promises = toResolve.map((item, idx) => { @@ -411,6 +398,7 @@ export class RemoteFileService extends FileService { name: fileStat.name, etag: fileStat.etag, mtime: fileStat.mtime, + isReadonly: fileStat.isReadonly }; }); }); @@ -431,23 +419,31 @@ export class RemoteFileService extends FileService { break; // we have hit a directory -> good } catch (e) { // ENOENT - basenames.push(posix.basename(directory.path)); - directory = directory.with({ path: posix.dirname(directory.path) }); + basenames.push(resources.basename(directory)); + directory = resources.dirname(directory); } } for (let i = basenames.length - 1; i >= 0; i--) { - directory = directory.with({ path: posix.join(directory.path, basenames[i]) }); + directory = resources.joinPath(directory, basenames[i]); await provider.mkdir(directory); } } + private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { + if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { + throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); + } + return provider; + } + createFile(resource: URI, content?: string, options?: ICreateFileOptions): TPromise { if (resource.scheme === Schemas.file) { return super.createFile(resource, content, options); } else { - return this._withProvider(resource).then(provider => { - return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => { + return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + + return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => { const encoding = this.encoding.getWriteEncoding(resource); return this._writeFile(provider, resource, new StringSnapshot(content), encoding, { create: true, overwrite: Boolean(options && options.overwrite) }); }); @@ -467,8 +463,8 @@ export class RemoteFileService extends FileService { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { - return this._withProvider(resource).then(provider => { - return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => { + return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => { const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value; return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true }); }); @@ -476,12 +472,12 @@ export class RemoteFileService extends FileService { } } - private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string, options: FileWriteOptions): TPromise { + private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string, options: FileWriteOptions): Promise { const readable = createReadableOfSnapshot(snapshot); const encoding = this.encoding.getWriteEncoding(resource, preferredEncoding); const encoder = encodeStream(encoding); const target = createWritableOfProvider(provider, resource, options); - return new TPromise((resolve, reject) => { + return new Promise((resolve, reject) => { readable.pipe(encoder).pipe(target); target.once('error', err => reject(err)); target.once('finish', _ => resolve(void 0)); @@ -490,15 +486,16 @@ export class RemoteFileService extends FileService { }); } - private static _asContent(content: IStreamContent): TPromise { - return new TPromise((resolve, reject) => { + private static _asContent(content: IStreamContent): Promise { + return new Promise((resolve, reject) => { let result: IContent = { value: '', encoding: content.encoding, etag: content.etag, mtime: content.mtime, name: content.name, - resource: content.resource + resource: content.resource, + isReadonly: content.isReadonly }; content.value.on('data', chunk => result.value += chunk); content.value.on('error', reject); @@ -508,24 +505,24 @@ export class RemoteFileService extends FileService { // --- delete - del(resource: URI, useTrash?: boolean): TPromise { + del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise { if (resource.scheme === Schemas.file) { - return super.del(resource, useTrash); + return super.del(resource, options); } else { - return this._withProvider(resource).then(provider => { - return provider.delete(resource).then(() => { + return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + return provider.delete(resource, { recursive: options && options.recursive }).then(() => { this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); }); }); } } - createFolder(resource: URI): TPromise { + createFolder(resource: URI): TPromise { if (resource.scheme === Schemas.file) { return super.createFolder(resource); } else { - return this._withProvider(resource).then(provider => { - return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => { + return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => { return provider.mkdir(resource).then(() => { return this.resolveFile(resource); }); @@ -537,15 +534,6 @@ export class RemoteFileService extends FileService { } } - rename(resource: URI, newName: string): TPromise { - if (resource.scheme === Schemas.file) { - return super.rename(resource, newName); - } else { - const target = resource.with({ path: posix.join(resource.path, '..', newName) }); - return this._doMoveWithInScheme(resource, target, false); - } - } - moveFile(source: URI, target: URI, overwrite?: boolean): TPromise { if (source.scheme !== target.scheme) { return this._doMoveAcrossScheme(source, target); @@ -559,10 +547,10 @@ export class RemoteFileService extends FileService { private _doMoveWithInScheme(source: URI, target: URI, overwrite?: boolean): TPromise { const prepare = overwrite - ? this.del(target).then(undefined, err => { /*ignore*/ }) + ? this.del(target, { recursive: true }).then(undefined, err => { /*ignore*/ }) : TPromise.as(null); - return prepare.then(() => this._withProvider(source)).then(provider => { + return prepare.then(() => this._withProvider(source)).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { return provider.rename(source, target, { overwrite }).then(() => { return this.resolveFile(target); }).then(fileStat => { @@ -580,7 +568,7 @@ export class RemoteFileService extends FileService { private _doMoveAcrossScheme(source: URI, target: URI, overwrite?: boolean): TPromise { return this.copyFile(source, target, overwrite).then(() => { - return this.del(source); + return this.del(source, { recursive: true }); }).then(() => { return this.resolveFile(target); }).then(fileStat => { @@ -594,7 +582,7 @@ export class RemoteFileService extends FileService { return super.copyFile(source, target, overwrite); } - return this._withProvider(target).then(provider => { + return this._withProvider(target).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { if (source.scheme === target.scheme && (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy)) { // good: provider supports copy withing scheme @@ -613,7 +601,7 @@ export class RemoteFileService extends FileService { } const prepare = overwrite - ? this.del(target).then(undefined, err => { /*ignore*/ }) + ? this.del(target, { recursive: true }).then(undefined, err => { /*ignore*/ }) : TPromise.as(null); return prepare.then(() => { @@ -648,7 +636,7 @@ export class RemoteFileService extends FileService { private _activeWatches = new Map, count: number }>(); - public watchFileChanges(resource: URI, opts?: IWatchOptions): void { + watchFileChanges(resource: URI, opts?: IWatchOptions): void { if (resource.scheme === Schemas.file) { return super.watchFileChanges(resource); } @@ -674,7 +662,7 @@ export class RemoteFileService extends FileService { }); } - public unwatchFileChanges(resource: URI): void { + unwatchFileChanges(resource: URI): void { if (resource.scheme === Schemas.file) { return super.unwatchFileChanges(resource); } diff --git a/src/vs/workbench/services/files/electron-browser/streams.ts b/src/vs/workbench/services/files/electron-browser/streams.ts index 8e56b7da30e..d28b16bab56 100644 --- a/src/vs/workbench/services/files/electron-browser/streams.ts +++ b/src/vs/workbench/services/files/electron-browser/streams.ts @@ -6,7 +6,7 @@ import { Readable, Writable } from 'stream'; import { UTF8 } from 'vs/base/node/encoding'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IFileSystemProvider, ITextSnapshot, FileSystemProviderCapabilities, FileWriteOptions } from 'vs/platform/files/common/files'; import { illegalArgument } from 'vs/base/common/errors'; diff --git a/src/vs/workbench/services/files/node/watcher/common.ts b/src/vs/workbench/services/files/node/watcher/common.ts index d191df60178..a9671c647c0 100644 --- a/src/vs/workbench/services/files/node/watcher/common.ts +++ b/src/vs/workbench/services/files/node/watcher/common.ts @@ -5,7 +5,7 @@ 'use strict'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts index b11e518b965..d3ef72006c8 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -9,11 +9,13 @@ import * as path from 'path'; import * as platform from 'vs/base/common/platform'; import * as watcher from 'vs/workbench/services/files/node/watcher/common'; import * as nsfw from 'vscode-nsfw'; -import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; -import { TPromise, ProgressCallback, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; +import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; +import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; import { normalizeNFC } from 'vs/base/common/normalization'; +import { Event, Emitter } from 'vs/base/common/event'; +import { realcaseSync, realpathSync } from 'vs/base/node/extfs'; const nsfwActionToRawChangeType: { [key: number]: number } = []; nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED; @@ -28,27 +30,22 @@ interface IWatcherObjet { interface IPathWatcher { ready: TPromise; watcher?: IWatcherObjet; - ignored: string[]; + ignored: glob.ParsedPattern[]; } export class NsfwWatcherService implements IWatcherService { private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private _pathWatchers: { [watchPath: string]: IPathWatcher } = {}; - private _watcherPromise: TPromise; - private _progressCallback: ProgressCallback; - private _errorCallback: ErrorCallback; private _verboseLogging: boolean; private enospcErrorLogged: boolean; - public initialize(verboseLogging: boolean): TPromise { - this._verboseLogging = true; - this._watcherPromise = new TPromise((c, e, p) => { - this._errorCallback = e; - this._progressCallback = p; + private _onWatchEvent = new Emitter(); + readonly onWatchEvent = this._onWatchEvent.event; - }); - return this._watcherPromise; + watch(options: IWatcherOptions): Event { + this._verboseLogging = options.verboseLogging; + return this.onWatchEvent; } private _watch(request: IWatcherRequest): void { @@ -58,7 +55,7 @@ export class NsfwWatcherService implements IWatcherService { let readyPromiseCallback: TValueCallback; this._pathWatchers[request.basePath] = { ready: new TPromise(c => readyPromiseCallback = c), - ignored: request.ignored + ignored: Array.isArray(request.ignored) ? request.ignored.map(ignored => glob.parse(ignored)) : [] }; process.on('uncaughtException', (e: Error | string) => { @@ -70,10 +67,38 @@ export class NsfwWatcherService implements IWatcherService { // See https://github.com/Microsoft/vscode/issues/7950 if (e === 'Inotify limit reached' && !this.enospcErrorLogged) { this.enospcErrorLogged = true; - this._errorCallback(new Error('Inotify limit reached (ENOSPC)')); + this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' }); } }); + // NSFW does not report file changes in the path provided on macOS if + // - the path uses wrong casing + // - the path is a symbolic link + // We have to detect this case and massage the events to correct this. + let realBasePathDiffers = false; + let realBasePathLength = request.basePath.length; + if (platform.isMacintosh) { + try { + + // First check for symbolic link + let realBasePath = realpathSync(request.basePath); + + // Second check for casing difference + if (request.basePath === realBasePath) { + realBasePath = (realcaseSync(request.basePath) || request.basePath); + } + + if (request.basePath !== realBasePath) { + realBasePathLength = realBasePath.length; + realBasePathDiffers = true; + + console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.basePath}, real: ${realBasePath})`); + } + } catch (error) { + // ignore + } + } + nsfw(request.basePath, events => { for (let i = 0; i < events.length; i++) { const e = events[i]; @@ -81,7 +106,7 @@ export class NsfwWatcherService implements IWatcherService { // Logging if (this._verboseLogging) { const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile) + ' -> ' + e.newFile : path.join(e.directory, e.file); - console.log(e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]', logPath); + console.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); } // Convert nsfw event to IRawFileChange and add to queue @@ -91,10 +116,14 @@ export class NsfwWatcherService implements IWatcherService { absolutePath = path.join(e.directory, e.oldFile); if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); + } else if (this._verboseLogging) { + console.log(' >> ignored', absolutePath); } absolutePath = path.join(e.directory, e.newFile); if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); + } else if (this._verboseLogging) { + console.log(' >> ignored', absolutePath); } } else { absolutePath = path.join(e.directory, e.file); @@ -103,6 +132,8 @@ export class NsfwWatcherService implements IWatcherService { type: nsfwActionToRawChangeType[e.action], path: absolutePath }); + } else if (this._verboseLogging) { + console.log(' >> ignored', absolutePath); } } } @@ -112,19 +143,27 @@ export class NsfwWatcherService implements IWatcherService { const events = undeliveredFileEvents; undeliveredFileEvents = []; - // Mac uses NFD unicode form on disk, but we want NFC if (platform.isMacintosh) { - events.forEach(e => e.path = normalizeNFC(e.path)); + events.forEach(e => { + + // Mac uses NFD unicode form on disk, but we want NFC + e.path = normalizeNFC(e.path); + + // Convert paths back to original form in case it differs + if (realBasePathDiffers) { + e.path = request.basePath + e.path.substr(realBasePathLength); + } + }); } // Broadcast to clients normalized const res = watcher.normalize(events); - this._progressCallback(res); + this._onWatchEvent.fire(res); // Logging if (this._verboseLogging) { res.forEach(r => { - console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); + console.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`); }); } @@ -169,7 +208,7 @@ export class NsfwWatcherService implements IWatcherService { // Refresh ignored arrays in case they changed roots.forEach(root => { if (root.basePath in this._pathWatchers) { - this._pathWatchers[root.basePath].ignored = root.ignored; + this._pathWatchers[root.basePath].ignored = Array.isArray(root.ignored) ? root.ignored.map(ignored => glob.parse(ignored)) : []; } }); @@ -186,7 +225,7 @@ export class NsfwWatcherService implements IWatcherService { })); } - private _isPathIgnored(absolutePath: string, ignored: string[]): boolean { - return ignored && ignored.some(ignore => glob.match(ignore, absolutePath)); + private _isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean { + return ignored && ignored.some(i => i(absolutePath)); } } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts index 7e571c74ea4..771933f3559 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts @@ -6,13 +6,23 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherRequest { basePath: string; ignored: string[]; } +export interface IWatcherOptions { + verboseLogging: boolean; +} + +export interface IWatchError { + message: string; +} + export interface IWatcherService { - initialize(verboseLogging: boolean): TPromise; + watch(options: IWatcherOptions): Event; setRoots(roots: IWatcherRequest[]): TPromise; } \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts index 88ebc3d19da..e90d6db72e0 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts @@ -6,22 +6,32 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWatcherRequest, IWatcherService } from './watcher'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherChannel extends IChannel { - call(command: 'initialize', verboseLogging: boolean): TPromise; + listen(event: 'watch', verboseLogging: boolean): Event; + listen(event: string, arg?: any): Event; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; - call(command: string, arg: any): TPromise; + call(command: string, arg?: any): TPromise; } export class WatcherChannel implements IWatcherChannel { constructor(private service: IWatcherService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'watch': return this.service.watch(arg); + } + throw new Error('No events'); + } + call(command: string, arg: any): TPromise { switch (command) { - case 'initialize': return this.service.initialize(arg); case 'setRoots': return this.service.setRoots(arg); } return undefined; @@ -32,8 +42,8 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - initialize(verboseLogging: boolean): TPromise { - return this.channel.call('initialize', verboseLogging); + watch(options: IWatcherOptions): Event { + return this.channel.listen('watch', options); } setRoots(roots: IWatcherRequest[]): TPromise { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts index e5403578de2..43c2324c8a4 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts @@ -5,10 +5,8 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; +import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import uri from 'vs/base/common/uri'; import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc'; import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; @@ -16,7 +14,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { filterEvent } from 'vs/base/common/event'; +import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export class FileWatcher { private static readonly MAX_RESTARTS = 5; @@ -24,7 +24,7 @@ export class FileWatcher { private service: WatcherChannelClient; private isDisposed: boolean; private restartCounter: number; - private toDispose: IDisposable[]; + private toDispose: IDisposable[] = []; constructor( private contextService: IWorkspaceContextService, @@ -35,17 +35,14 @@ export class FileWatcher { ) { this.isDisposed = false; this.restartCounter = 0; - this.toDispose = []; } public startWatching(): () => void { - const args = ['--type=watcherService']; - const client = new Client( - uri.parse(require.toUrl('bootstrap')).fsPath, + getPathFromAmdModule(require, 'bootstrap-fork'), { - serverName: 'Watcher', - args, + serverName: 'File Watcher (nsfw)', + args: ['--type=watcherService'], env: { AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp', PIPE_LOGGING: 'true', @@ -55,16 +52,7 @@ export class FileWatcher { ); this.toDispose.push(client); - // Initialize watcher - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - this.service.initialize(this.verboseLogging).then(null, err => { - if (!this.isDisposed && !isPromiseCanceledError(err)) { - return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up - } - return void 0; - }, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => { - + client.onDidProcessExit(() => { // our watcher app should never be completed because it keeps on watching. being in here indicates // that the watcher process died and we want to restart it here. we only do it a max number of times if (!this.isDisposed) { @@ -76,11 +64,20 @@ export class FileWatcher { this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); } } - }, error => { - if (!this.isDisposed) { - this.errorLogger(error); - } - }); + }, null, this.toDispose); + + // Initialize watcher + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed); + + const onError = filterEvent(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + onError(err => this.errorLogger(err.message), null, this.toDispose); + + const onFileChanges = filterEvent(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); + onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); // Start watching this.updateFolders(); @@ -118,17 +115,6 @@ export class FileWatcher { })); } - private onRawFileEvents(events: IRawFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onFileChanges(toFileChangesEvent(events)); - } - } - private dispose(): void { this.isDisposed = true; this.toDispose = dispose(this.toDispose); diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index 550297d06f0..090f688ef85 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -21,7 +21,8 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import { realcaseSync } from 'vs/base/node/extfs'; import { isMacintosh } from 'vs/base/common/platform'; import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common'; -import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { Emitter, Event } from 'vs/base/common/event'; interface IWatcher { requests: ExtendedWatcherRequest[]; @@ -44,29 +45,20 @@ export class ChokidarWatcherService implements IWatcherService { private _watchers: { [watchPath: string]: IWatcher }; private _watcherCount: number; - private _watcherPromise: TPromise; private _options: IWatcherOptions & IChockidarWatcherOptions; private spamCheckStartTime: number; private spamWarningLogged: boolean; private enospcErrorLogged: boolean; - private _errorCallback: (error: Error) => void; - private _fileChangeCallback: (changes: watcherCommon.IRawFileChange[]) => void; - public initialize(options: IWatcherOptions & IChockidarWatcherOptions): TPromise { + private _onWatchEvent = new Emitter(); + readonly onWatchEvent = this._onWatchEvent.event; + + watch(options: IWatcherOptions & IChockidarWatcherOptions): Event { this._options = options; this._watchers = Object.create(null); this._watcherCount = 0; - this._watcherPromise = new TPromise((c, e, p) => { - this._errorCallback = (error) => { - this.stop(); - e(error); - }; - this._fileChangeCallback = p; - }, () => { - this.stop(); - }); - return this._watcherPromise; + return this.onWatchEvent; } public setRoots(requests: IWatcherRequest[]): TPromise { @@ -121,7 +113,8 @@ export class ChokidarWatcherService implements IWatcherService { }; // if there's only one request, use the built-in ignore-filterering - if (requests.length === 1) { + const isSingleFolder = requests.length === 1; + if (isSingleFolder) { watcherOpts.ignored = requests[0].ignored; } @@ -202,15 +195,19 @@ export class ChokidarWatcherService implements IWatcherService { return; } - if (isIgnored(path, watcher.requests)) { - return; + // if there's more than one request we need to do + // extra filtering due to potentially overlapping roots + if (!isSingleFolder) { + if (isIgnored(path, watcher.requests)) { + return; + } } let event = { type: eventType, path }; // Logging if (this._options.verboseLogging) { - console.log(eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', path); + console.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); } // Check for spam @@ -233,12 +230,12 @@ export class ChokidarWatcherService implements IWatcherService { // Broadcast to clients normalized const res = watcherCommon.normalize(events); - this._fileChangeCallback(res); + this._onWatchEvent.fire(res); // Logging if (this._options.verboseLogging) { res.forEach(r => { - console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); + console.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`); }); } @@ -257,7 +254,8 @@ export class ChokidarWatcherService implements IWatcherService { if ((error).code === 'ENOSPC') { if (!this.enospcErrorLogged) { this.enospcErrorLogged = true; - this._errorCallback(new Error('Inotify limit reached (ENOSPC)')); + this.stop(); + this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' }); } } else { console.error(error.toString()); diff --git a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts index 9f9ce6309c7..ef26ad06b85 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -137,19 +137,15 @@ suite.skip('Chockidar watching', () => { await pfs.mkdirp(bFolder); await pfs.mkdirp(b2Folder); - const promise = service.initialize({ verboseLogging: false, pollingInterval: 200 }); - promise.then(null, - e => { - console.log('set error', e); - error = e; - }, - p => { - if (Array.isArray(p)) { - result.push(...p); - } + const opts = { verboseLogging: false, pollingInterval: 200 }; + service.watch(opts)(e => { + if (Array.isArray(e)) { + result.push(...e); + } else { + console.log('set error', e.message); + error = e.message; } - ); - + }); }); suiteTeardown(async () => { diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts index 8ed35e6c792..771933f3559 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts @@ -6,6 +6,8 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherRequest { basePath: string; @@ -16,7 +18,11 @@ export interface IWatcherOptions { verboseLogging: boolean; } -export interface IWatcherService { - initialize(options: IWatcherOptions): TPromise; - setRoots(roots: IWatcherRequest[]): TPromise; +export interface IWatchError { + message: string; } + +export interface IWatcherService { + watch(options: IWatcherOptions): Event; + setRoots(roots: IWatcherRequest[]): TPromise; +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts index bf2fba4664b..e90d6db72e0 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts @@ -6,22 +6,32 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; +import { Event } from 'vs/base/common/event'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; export interface IWatcherChannel extends IChannel { - call(command: 'initialize', options: IWatcherOptions): TPromise; + listen(event: 'watch', verboseLogging: boolean): Event; + listen(event: string, arg?: any): Event; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; - call(command: string, arg: any): TPromise; + call(command: string, arg?: any): TPromise; } export class WatcherChannel implements IWatcherChannel { constructor(private service: IWatcherService) { } + listen(event: string, arg?: any): Event { + switch (event) { + case 'watch': return this.service.watch(arg); + } + throw new Error('No events'); + } + call(command: string, arg: any): TPromise { switch (command) { - case 'initialize': return this.service.initialize(arg); case 'setRoots': return this.service.setRoots(arg); } return undefined; @@ -32,8 +42,8 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - initialize(options: IWatcherOptions): TPromise { - return this.channel.call('initialize', options); + watch(options: IWatcherOptions): Event { + return this.channel.listen('watch', options); } setRoots(roots: IWatcherRequest[]): TPromise { diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts index e51c287854f..1c9aff61c65 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts @@ -5,18 +5,18 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; +import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import uri from 'vs/base/common/uri'; import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc'; import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Schemas } from 'vs/base/common/network'; +import { filterEvent } from 'vs/base/common/event'; +import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export class FileWatcher { private static readonly MAX_RESTARTS = 5; @@ -42,9 +42,9 @@ export class FileWatcher { const args = ['--type=watcherService']; const client = new Client( - uri.parse(require.toUrl('bootstrap')).fsPath, + getPathFromAmdModule(require, 'bootstrap-fork'), { - serverName: 'Watcher', + serverName: 'File Watcher (chokidar)', args, env: { AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/unix/watcherApp', @@ -55,20 +55,7 @@ export class FileWatcher { ); this.toDispose.push(client); - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - - const options = { - verboseLogging: this.verboseLogging - }; - - this.service.initialize(options).then(null, err => { - if (!this.isDisposed && !isPromiseCanceledError(err)) { - return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up - } - return void 0; - }, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => { - + client.onDidProcessExit(() => { // our watcher app should never be completed because it keeps on watching. being in here indicates // that the watcher process died and we want to restart it here. we only do it a max number of times if (!this.isDisposed) { @@ -80,11 +67,19 @@ export class FileWatcher { this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); } } - }, error => { - if (!this.isDisposed) { - this.errorLogger(error); - } - }); + }, null, this.toDispose); + + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed); + + const onError = filterEvent(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + onError(err => this.errorLogger(err.message), null, this.toDispose); + + const onFileChanges = filterEvent(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); + onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); // Start watching this.updateFolders(); @@ -123,19 +118,8 @@ export class FileWatcher { })); } - private onRawFileEvents(events: IRawFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onFileChanges(toFileChangesEvent(events)); - } - } - private dispose(): void { this.isDisposed = true; this.toDispose = dispose(this.toDispose); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts b/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts index 949c3d1b36f..044fd96d817 100644 --- a/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts @@ -10,9 +10,9 @@ import * as cp from 'child_process'; import { FileChangeType } from 'vs/platform/files/common/files'; import * as decoder from 'vs/base/node/decoder'; import * as glob from 'vs/base/common/glob'; -import uri from 'vs/base/common/uri'; import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export class OutOfProcessWin32FolderWatcher { @@ -20,18 +20,26 @@ export class OutOfProcessWin32FolderWatcher { private static changeTypeMap: FileChangeType[] = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED]; + private ignored: glob.ParsedPattern[]; + private handle: cp.ChildProcess; private restartCounter: number; constructor( private watchedFolder: string, - private ignored: string[], + ignored: string[], private eventCallback: (events: IRawFileChange[]) => void, private errorCallback: (error: string) => void, private verboseLogging: boolean ) { this.restartCounter = 0; + if (Array.isArray(ignored)) { + this.ignored = ignored.map(i => glob.parse(i)); + } else { + this.ignored = []; + } + this.startWatcher(); } @@ -41,12 +49,12 @@ export class OutOfProcessWin32FolderWatcher { args.push('-verbose'); } - this.handle = cp.spawn(uri.parse(require.toUrl('vs/workbench/services/files/node/watcher/win32/CodeHelper.exe')).fsPath, args); + this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files/node/watcher/win32/CodeHelper.exe'), args); const stdoutLineDecoder = new decoder.LineDecoder(); // Events over stdout - this.handle.stdout.on('data', (data: NodeBuffer) => { + this.handle.stdout.on('data', (data: Buffer) => { // Collect raw events from output const rawEvents: IRawFileChange[] = []; @@ -60,7 +68,11 @@ export class OutOfProcessWin32FolderWatcher { if (changeType >= 0 && changeType < 3) { // Support ignores - if (this.ignored && this.ignored.some(ignore => glob.match(ignore, absolutePath))) { + if (this.ignored && this.ignored.some(ignore => ignore(absolutePath))) { + if (this.verboseLogging) { + console.log('%c[File Watcher (C#)]', 'color: blue', ' >> ignored', absolutePath); + } + return; } @@ -73,7 +85,7 @@ export class OutOfProcessWin32FolderWatcher { // 3 Logging else { - console.log('%c[File Watcher]', 'color: darkgreen', eventParts[1]); + console.log('%c[File Watcher (C#)]', 'color: blue', eventParts[1]); } } }); @@ -86,26 +98,26 @@ export class OutOfProcessWin32FolderWatcher { // Errors this.handle.on('error', (error: Error) => this.onError(error)); - this.handle.stderr.on('data', (data: NodeBuffer) => this.onError(data)); + this.handle.stderr.on('data', (data: Buffer) => this.onError(data)); // Exit this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal)); } - private onError(error: Error | NodeBuffer): void { - this.errorCallback('[FileWatcher] process error: ' + error.toString()); + private onError(error: Error | Buffer): void { + this.errorCallback('[File Watcher (C#)] process error: ' + error.toString()); } private onExit(code: number, signal: string): void { if (this.handle) { // exit while not yet being disposed is unexpected! - this.errorCallback(`[FileWatcher] terminated unexpectedly (code: ${code}, signal: ${signal})`); + this.errorCallback(`[File Watcher (C#)] terminated unexpectedly (code: ${code}, signal: ${signal})`); if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) { - this.errorCallback('[FileWatcher] is restarted again...'); + this.errorCallback('[File Watcher (C#)] is restarted again...'); this.restartCounter++; this.startWatcher(); // restart } else { - this.errorCallback('[FileWatcher] Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!'); + this.errorCallback('[File Watcher (C#)] Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!'); } } } @@ -116,4 +128,4 @@ export class OutOfProcessWin32FolderWatcher { this.handle = null; } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts index 26295cbc4a9..3672cdd0775 100644 --- a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts @@ -12,6 +12,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { normalize } from 'path'; import { rtrim, endsWith } from 'vs/base/common/strings'; import { sep } from 'vs/base/common/paths'; +import { Schemas } from 'vs/base/common/network'; export class FileWatcher { private isDisposed: boolean; @@ -26,6 +27,9 @@ export class FileWatcher { } public startWatching(): () => void { + if (this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file) { + return () => { }; + } let basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath); if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, sep)) { diff --git a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index 822b9dc356d..9aa87fbfdde 100644 --- a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -13,7 +13,7 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import * as uuid from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; import * as encodingLib from 'vs/base/node/encoding'; @@ -24,6 +24,7 @@ import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/work import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IEncodingOverride } from 'vs/workbench/services/files/electron-browser/encoding'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; suite('FileService', () => { let service: FileService; @@ -33,10 +34,10 @@ suite('FileService', () => { setup(function () { const id = uuid.generateUuid(); testDir = path.join(parentDir, id); - const sourceDir = require.toUrl('./fixtures/service'); + const sourceDir = getPathFromAmdModule(require, './fixtures/service'); return pfs.copy(sourceDir, testDir).then(() => { - service = new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); + service = new FileService(new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); }); }); @@ -157,7 +158,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'index.html')); return service.resolveFile(resource).then(source => { - return service.rename(source.resource, 'other.html').then(renamed => { + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'other.html'))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.equal(fs.existsSync(source.resource.fsPath), false); @@ -181,7 +182,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'index.html')); return service.resolveFile(resource).then(source => { - return service.rename(source.resource, renameToPath).then(renamed => { + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), renameToPath))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.equal(fs.existsSync(source.resource.fsPath), false); @@ -202,7 +203,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'deep')); return service.resolveFile(resource).then(source => { - return service.rename(source.resource, 'deeper').then(renamed => { + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'deeper'))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.equal(fs.existsSync(source.resource.fsPath), false); @@ -226,7 +227,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'deep')); return service.resolveFile(resource).then(source => { - return service.rename(source.resource, renameToPath).then(renamed => { + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), renameToPath))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.equal(fs.existsSync(source.resource.fsPath), false); @@ -246,7 +247,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'index.html')); return service.resolveFile(resource).then(source => { - return service.rename(source.resource, 'INDEX.html').then(renamed => { + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'INDEX.html'))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.equal(path.basename(renamed.resource.fsPath), 'INDEX.html'); @@ -430,7 +431,7 @@ suite('FileService', () => { test('copyFile - MIX CASE', function () { return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { - return service.rename(source.resource, 'CONWAY.js').then(renamed => { // index.html => CONWAY.js + return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'CONWAY.js'))).then(renamed => { assert.equal(fs.existsSync(renamed.resource.fsPath), true); assert.ok(fs.readdirSync(testDir).some(f => f === 'CONWAY.js')); @@ -476,7 +477,7 @@ suite('FileService', () => { }); }); - test('deleteFolder', function () { + test('deleteFolder (recursive)', function () { let event: FileOperationEvent; const toDispose = service.onAfterOperation(e => { event = e; @@ -484,7 +485,7 @@ suite('FileService', () => { const resource = uri.file(path.join(testDir, 'deep')); return service.resolveFile(resource).then(source => { - return service.del(source.resource).then(() => { + return service.del(source.resource, { recursive: true }).then(() => { assert.equal(fs.existsSync(source.resource.fsPath), false); assert.ok(event); @@ -495,6 +496,17 @@ suite('FileService', () => { }); }); + test('deleteFolder (non recursive)', function () { + const resource = uri.file(path.join(testDir, 'deep')); + return service.resolveFile(resource).then(source => { + return service.del(source.resource).then(() => { + return TPromise.wrapError(new Error('Unexpected')); + }, error => { + return TPromise.as(true); + }); + }); + }); + test('resolveFile', function () { return service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).then(r => { assert.equal(r.children.length, 8); @@ -826,7 +838,7 @@ suite('FileService', () => { // setup const _id = uuid.generateUuid(); const _testDir = path.join(parentDir, _id); - const _sourceDir = require.toUrl('./fixtures/service'); + const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); return pfs.copy(_sourceDir, _testDir).then(() => { const encodingOverride: IEncodingOverride[] = []; @@ -841,7 +853,7 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); const _service = new FileService( - new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, textResourceConfigurationService, configurationService, @@ -871,7 +883,7 @@ suite('FileService', () => { // setup const _id = uuid.generateUuid(); const _testDir = path.join(parentDir, _id); - const _sourceDir = require.toUrl('./fixtures/service'); + const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); return pfs.copy(_sourceDir, _testDir).then(() => { const encodingOverride: IEncodingOverride[] = []; @@ -886,7 +898,7 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); const _service = new FileService( - new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, textResourceConfigurationService, configurationService, @@ -916,11 +928,11 @@ suite('FileService', () => { // setup const _id = uuid.generateUuid(); const _testDir = path.join(parentDir, _id); - const _sourceDir = require.toUrl('./fixtures/service'); + const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); const resource = uri.file(path.join(testDir, 'index.html')); const _service = new FileService( - new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), diff --git a/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts index 3a9c19ad869..df78a28b30b 100644 --- a/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts @@ -10,12 +10,13 @@ import * as path from 'path'; import * as assert from 'assert'; import { StatResolver } from 'vs/workbench/services/files/electron-browser/fileService'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { isLinux } from 'vs/base/common/platform'; import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; function create(relativePath: string): StatResolver { - let basePath = require.toUrl('./fixtures/resolver'); + let basePath = getPathFromAmdModule(require, './fixtures/resolver'); let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath; let fsStat = fs.statSync(absolutePath); @@ -23,7 +24,7 @@ function create(relativePath: string): StatResolver { } function toResource(relativePath: string): uri { - let basePath = require.toUrl('./fixtures/resolver'); + let basePath = getPathFromAmdModule(require, './fixtures/resolver'); let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath; return uri.file(absolutePath); diff --git a/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts b/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts index 175a45b4f40..186e9c33e0b 100644 --- a/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts @@ -9,7 +9,7 @@ import * as assert from 'assert'; import * as platform from 'vs/base/common/platform'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; -import uri from 'vs/base/common/uri'; +import { URI as uri } from 'vs/base/common/uri'; import { IRawFileChange, toFileChangesEvent, normalize } from 'vs/workbench/services/files/node/watcher/common'; import { Event, Emitter } from 'vs/base/common/event'; @@ -223,4 +223,4 @@ suite('Watcher', () => { watch.report(raw); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/group/common/editorGroupsService.ts b/src/vs/workbench/services/group/common/editorGroupsService.ts index 138849ffa8e..2efb09b9e63 100644 --- a/src/vs/workbench/services/group/common/editorGroupsService.ts +++ b/src/vs/workbench/services/group/common/editorGroupsService.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur export const IEditorGroupsService = createDecorator('editorGroupsService'); -export enum GroupDirection { +export const enum GroupDirection { UP, DOWN, LEFT, @@ -31,12 +31,12 @@ export function preferredSideBySideGroupDirection(configurationService: IConfigu return GroupDirection.RIGHT; } -export enum GroupOrientation { +export const enum GroupOrientation { HORIZONTAL, VERTICAL } -export enum GroupLocation { +export const enum GroupLocation { FIRST, LAST, NEXT, @@ -48,7 +48,7 @@ export interface IFindGroupScope { location?: GroupLocation; } -export enum GroupsArrangement { +export const enum GroupsArrangement { /** * Make the current active group consume the maximum @@ -64,7 +64,7 @@ export enum GroupsArrangement { export interface GroupLayoutArgument { size?: number; - groups?: Array; + groups?: GroupLayoutArgument[]; } export interface EditorGroupLayout { @@ -84,7 +84,7 @@ export interface IAddGroupOptions { activate?: boolean; } -export enum MergeGroupMode { +export const enum MergeGroupMode { COPY_EDITORS, MOVE_EDITORS } @@ -106,7 +106,7 @@ export interface IEditorReplacement { options?: IEditorOptions | ITextEditorOptions; } -export enum GroupsOrder { +export const enum GroupsOrder { /** * Groups sorted by creation order (oldest one first) @@ -124,7 +124,7 @@ export enum GroupsOrder { GRID_APPEARANCE } -export enum EditorsOrder { +export const enum EditorsOrder { /** * Editors sorted by most recent activity (most recent active first) @@ -290,7 +290,7 @@ export interface IEditorGroupsService { copyGroup(group: IEditorGroup | GroupIdentifier, location: IEditorGroup | GroupIdentifier, direction: GroupDirection): IEditorGroup; } -export enum GroupChangeKind { +export const enum GroupChangeKind { /* Group Changes */ GROUP_ACTIVE, @@ -476,4 +476,4 @@ export interface IEditorGroup { * Invoke a function in the context of the services of this group. */ invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T; -} \ No newline at end of file +} 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 b20b30603a9..6fec3144911 100644 --- a/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/group/test/browser/editorGroupsService.test.ts @@ -15,7 +15,7 @@ import { IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorInput, IFileEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorOptions, CloseDirection } from 'vs/workbench/common/editor'; import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, Extensions, EditorDescriptor } from 'vs/workbench/browser/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; diff --git a/src/vs/workbench/services/history/common/history.ts b/src/vs/workbench/services/history/common/history.ts index 9822fecb596..0497fc99f37 100644 --- a/src/vs/workbench/services/history/common/history.ts +++ b/src/vs/workbench/services/history/common/history.ts @@ -7,7 +7,7 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IEditorInput } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export const IHistoryService = createDecorator('historyService'); @@ -60,12 +60,14 @@ export interface IHistoryService { * Looking at the editor history, returns the workspace root of the last file that was * inside the workspace and part of the editor history. * - * @param schemeFilter optional filter to restrict roots by scheme. + * @param schemeFilter filter to restrict roots by scheme. */ - getLastActiveWorkspaceRoot(schemeFilter?: string): URI; + getLastActiveWorkspaceRoot(schemeFilter: string): URI; /** - * Looking at the editor history, returns the resource of the last file tht was opened. + * Looking at the editor history, returns the resource of the last file that was opened. + * + * @param schemeFilter filter to restrict roots by scheme. */ - getLastActiveFile(): URI; + getLastActiveFile(schemeFilter: string): URI; } \ No newline at end of file diff --git a/src/vs/workbench/services/history/electron-browser/history.ts b/src/vs/workbench/services/history/electron-browser/history.ts index 10b941f25af..9991163267b 100644 --- a/src/vs/workbench/services/history/electron-browser/history.ts +++ b/src/vs/workbench/services/history/electron-browser/history.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as errors from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, Extensions as EditorInputExtensions, IFileInputFactory, IEditorIdentifier } from 'vs/workbench/common/editor'; @@ -16,7 +16,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files'; import { Selection } from 'vs/editor/common/core/selection'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -30,8 +30,9 @@ import { IExpression } from 'vs/base/common/glob'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources'; -import { Schemas } from 'vs/base/common/network'; 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'; /** * Stores the selection & view state of an editor and allows to compare it to other selection states. @@ -49,15 +50,15 @@ export class TextEditorState { } : void 0; } - public get editorInput(): IEditorInput { + get editorInput(): IEditorInput { return this._editorInput; } - public get selection(): ITextEditorSelection { + get selection(): ITextEditorSelection { return this.textEditorSelection; } - public justifiesNewPushState(other: TextEditorState, event?: ICursorPositionChangedEvent): boolean { + justifiesNewPushState(other: TextEditorState, event?: ICursorPositionChangedEvent): boolean { if (event && event.source === 'api') { return true; // always let API source win (e.g. "Go to definition" should add a history entry) } @@ -97,17 +98,15 @@ interface IRecentlyClosedFile { index: number; } -export class HistoryService implements IHistoryService { +export class HistoryService extends Disposable implements IHistoryService { - public _serviceBrand: any; + _serviceBrand: any; private static readonly STORAGE_KEY = 'history.entries'; private static readonly MAX_HISTORY_ITEMS = 200; private static readonly MAX_STACK_ITEMS = 20; private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20; - private toUnbind: IDisposable[]; - private activeEditorListeners: IDisposable[]; private lastActiveEditor: IEditorIdentifier; @@ -124,6 +123,9 @@ export class HistoryService implements IHistoryService { private fileInputFactory: IFileInputFactory; + private canNavigateBackContextKey: IContextKey; + private canNavigateForwardContextKey: IContextKey; + constructor( @IEditorService private editorService: EditorServiceImpl, @IEditorGroupsService private editorGroupService: IEditorGroupsService, @@ -134,10 +136,16 @@ export class HistoryService implements IHistoryService { @IFileService private fileService: IFileService, @IWindowsService private windowService: IWindowsService, @IInstantiationService private instantiationService: IInstantiationService, + @IPartService private partService: IPartService, + @IContextKeyService private contextKeyService: IContextKeyService ) { - this.toUnbind = []; + super(); + this.activeEditorListeners = []; + this.canNavigateBackContextKey = (new RawContextKey('canNavigateBack', false)).bindTo(this.contextKeyService); + this.canNavigateForwardContextKey = (new RawContextKey('canNavigateForward', false)).bindTo(this.contextKeyService); + this.fileInputFactory = Registry.as(EditorInputExtensions.EditorInputFactories).getFileInputFactory(); this.index = -1; @@ -145,11 +153,11 @@ export class HistoryService implements IHistoryService { this.stack = []; this.recentlyClosedFiles = []; this.loaded = false; - this.resourceFilter = instantiationService.createInstance( + this.resourceFilter = this._register(instantiationService.createInstance( ResourceGlobMatcher, (root: URI) => this.getExcludes(root), (event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration('search.exclude') - ); + )); this.registerListeners(); } @@ -161,12 +169,12 @@ export class HistoryService implements IHistoryService { } private registerListeners(): void { - this.toUnbind.push(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); - this.toUnbind.push(this.editorService.onDidOpenEditorFail(event => this.remove(event.editor))); - this.toUnbind.push(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event))); - this.toUnbind.push(this.lifecycleService.onShutdown(reason => this.saveHistory())); - this.toUnbind.push(this.fileService.onFileChanges(event => this.onFileChanges(event))); - this.toUnbind.push(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange())); + this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); + this._register(this.editorService.onDidOpenEditorFail(event => this.remove(event.editor))); + this._register(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event))); + this._register(this.lifecycleService.onShutdown(reason => this.saveHistory())); + this._register(this.fileService.onFileChanges(event => this.onFileChanges(event))); + this._register(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange())); } private onActiveEditorChanged(): void { @@ -236,7 +244,7 @@ export class HistoryService implements IHistoryService { } } - public reopenLastClosedEditor(): void { + reopenLastClosedEditor(): void { this.ensureHistoryLoaded(); let lastClosedFile = this.recentlyClosedFiles.pop(); @@ -249,7 +257,7 @@ export class HistoryService implements IHistoryService { } } - public forward(acrossEditors?: boolean): void { + forward(acrossEditors?: boolean): void { if (this.stack.length > this.index + 1) { if (acrossEditors) { this.doForwardAcrossEditors(); @@ -267,6 +275,8 @@ export class HistoryService implements IHistoryService { private setIndex(value: number): void { this.lastIndex = this.index; this.index = value; + + this.updateContextKeys(); } private doForwardAcrossEditors(): void { @@ -287,7 +297,7 @@ export class HistoryService implements IHistoryService { } } - public back(acrossEditors?: boolean): void { + back(acrossEditors?: boolean): void { if (this.index > 0) { if (acrossEditors) { this.doBackAcrossEditors(); @@ -297,7 +307,7 @@ export class HistoryService implements IHistoryService { } } - public last(): void { + last(): void { if (this.lastIndex === -1) { this.back(); } else { @@ -329,7 +339,7 @@ export class HistoryService implements IHistoryService { } } - public clear(): void { + clear(): void { this.ensureHistoryLoaded(); this.index = -1; @@ -337,6 +347,13 @@ export class HistoryService implements IHistoryService { this.stack.splice(0); this.history = []; this.recentlyClosedFiles = []; + + this.updateContextKeys(); + } + + private updateContextKeys(): void { + this.canNavigateBackContextKey.set(this.stack.length > 0 && this.index > 0); + this.canNavigateForwardContextKey.set(this.stack.length > 0 && this.index < this.stack.length - 1); } private navigate(acrossEditors?: boolean): void { @@ -362,7 +379,7 @@ export class HistoryService implements IHistoryService { openEditorPromise = this.editorService.openEditor({ resource: (entry.input as IResourceInput).resource, options }); } - openEditorPromise.done(() => { + openEditorPromise.then(() => { this.navigatingInStack = false; }, error => { this.navigatingInStack = false; @@ -421,9 +438,9 @@ export class HistoryService implements IHistoryService { this.removeExcludedFromHistory(); } - public remove(input: IEditorInput | IResourceInput): void; - public remove(input: FileChangesEvent): void; - public remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { + remove(input: IEditorInput | IResourceInput): void; + remove(input: FileChangesEvent): void; + remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { this.removeFromHistory(arg1); this.removeFromStack(arg1); this.removeFromRecentlyClosedFiles(arg1); @@ -502,7 +519,7 @@ export class HistoryService implements IHistoryService { this.add(editor.input); } - public add(input: IEditorInput, selection?: ITextEditorSelection): void { + add(input: IEditorInput, selection?: ITextEditorSelection): void { if (!this.navigatingInStack) { this.addOrReplaceInStack(input, selection); } @@ -568,6 +585,9 @@ export class HistoryService implements IHistoryService { if (stackInput instanceof EditorInput) { once(stackInput.onDispose)(() => this.removeFromStack(input)); } + + // Context + this.updateContextKeys(); } private preferResourceInput(input: IEditorInput): IEditorInput | IResourceInput { @@ -594,6 +614,8 @@ export class HistoryService implements IHistoryService { this.stack = this.stack.filter(e => !this.matches(arg1, e.input)); this.index = this.stack.length - 1; // reset index this.lastIndex = -1; + + this.updateContextKeys(); } private removeFromRecentlyClosedFiles(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { @@ -607,7 +629,7 @@ export class HistoryService implements IHistoryService { const input = arg1 as IResourceInput; - this.windowService.removeFromRecentlyOpened([input.resource.fsPath]); + this.windowService.removeFromRecentlyOpened([input.resource]); } private isFileOpened(resource: URI, group: IEditorGroup): boolean { @@ -658,8 +680,15 @@ export class HistoryService implements IHistoryService { if (arg2 instanceof EditorInput) { const inputResource = arg2.getResource(); + if (!inputResource) { + return false; + } - return inputResource && this.fileService.canHandleResource(inputResource) && inputResource.toString() === resource.toString(); + if (this.partService.isCreated() && !this.fileService.canHandleResource(inputResource)) { + return false; // make sure to only check this when workbench has started (for https://github.com/Microsoft/vscode/issues/48275) + } + + return inputResource.toString() === resource.toString(); } const resourceInput = arg2 as IResourceInput; @@ -667,7 +696,7 @@ export class HistoryService implements IHistoryService { return resourceInput && resourceInput.resource.toString() === resource.toString(); } - public getHistory(): (IEditorInput | IResourceInput)[] { + getHistory(): (IEditorInput | IResourceInput)[] { this.ensureHistoryLoaded(); return this.history.slice(0); @@ -748,7 +777,7 @@ export class HistoryService implements IHistoryService { }).filter(input => !!input); } - public getLastActiveWorkspaceRoot(schemeFilter?: string): URI { + getLastActiveWorkspaceRoot(schemeFilter: string): URI { // No Folder: return early const folders = this.contextService.getWorkspace().folders; @@ -796,27 +825,23 @@ export class HistoryService implements IHistoryService { return void 0; } - public getLastActiveFile(): URI { + getLastActiveFile(schemeFilter: string): URI { const history = this.getHistory(); for (let i = 0; i < history.length; i++) { let resource: URI; const input = history[i]; if (input instanceof EditorInput) { - resource = toResource(input, { filter: Schemas.file }); + resource = toResource(input, { filter: schemeFilter }); } else { resource = (input as IResourceInput).resource; } - if (resource && resource.scheme === Schemas.file) { + if (resource && resource.scheme === schemeFilter) { return resource; } } return void 0; } - - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); - } } diff --git a/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts index a3e9e3727bd..3ef280db960 100644 --- a/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts +++ b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IssueReporterStyles, IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue'; +import { IssueReporterStyles, IIssueService, IssueReporterData, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -14,6 +14,7 @@ import { IExtensionManagementService, IExtensionEnablementService, LocalExtensio import { webFrame } from 'electron'; import { assign } from 'vs/base/common/objects'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWindowService } from 'vs/platform/windows/common/windows'; export class WorkbenchIssueService implements IWorkbenchIssueService { _serviceBrand: any; @@ -22,7 +23,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IIssueService private issueService: IIssueService, @IThemeService private themeService: IThemeService, @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, + @IWindowService private windowService: IWindowService ) { } @@ -44,7 +46,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { openProcessExplorer(): TPromise { const theme = this.themeService.getTheme(); - const data = { + const data: ProcessExplorerData = { + pid: this.windowService.getConfiguration().mainPid, zoomLevel: webFrame.getZoomLevel(), styles: { backgroundColor: theme.getColor(editorBackground) && theme.getColor(editorBackground).toString(), diff --git a/src/vs/workbench/services/jsonschemas/common/jsonValidationExtensionPoint.ts b/src/vs/workbench/services/jsonschemas/common/jsonValidationExtensionPoint.ts index 38e859052ba..4169ec2f48c 100644 --- a/src/vs/workbench/services/jsonschemas/common/jsonValidationExtensionPoint.ts +++ b/src/vs/workbench/services/jsonschemas/common/jsonValidationExtensionPoint.ts @@ -6,9 +6,8 @@ import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import URI from 'vs/base/common/uri'; import * as strings from 'vs/base/common/strings'; -import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; interface IJSONValidationExtensionPoint { fileMatch: string; @@ -60,8 +59,10 @@ export class JSONValidationExtensionPoint { } if (strings.startsWith(uri, './')) { try { - //TODO@extensionLocation - uri = URI.file(paths.normalize(paths.join(extensionLocation.fsPath, uri))).toString(); + const colorThemeLocation = resources.joinPath(extensionLocation, uri); + if (!resources.isEqualOrParent(colorThemeLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.url` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", configurationExtPoint.name, colorThemeLocation.toString(), extensionLocation.path)); + } } catch (e) { collector.error(nls.localize('invalid.url.fileschema', "'configuration.jsonValidation.url' is an invalid relative URL: {0}", e.message)); } diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index 98b80945993..374c8b468f5 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { isArray } from 'vs/base/common/types'; import { Queue } from 'vs/base/common/async'; @@ -74,10 +74,11 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding return this.resolveAndValidate() .then(reference => { const model = reference.object.textEditorModel; - if (keybindingItem.isDefault) { - this.updateDefaultKeybinding(key, keybindingItem, model); - } else { - this.updateUserKeybinding(key, keybindingItem, model); + const userKeybindingEntries = json.parse(model.getValue()); + const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries); + this.updateKeybinding(key, keybindingItem, model, userKeybindingEntryIndex); + if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) { + this.removeDefaultKeybinding(keybindingItem, model); } return this.save().then(() => reference.dispose()); }); @@ -112,32 +113,16 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding return this.textFileService.save(this.resource); } - private updateUserKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel): void { + private updateKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel, userKeybindingEntryIndex: number): void { const { tabSize, insertSpaces } = model.getOptions(); const eol = model.getEOL(); - const userKeybindingEntries = json.parse(model.getValue()); - const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries); - if (userKeybindingEntryIndex !== -1) { - this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model); - } - } - - private updateDefaultKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: ITextModel): void { - const { tabSize, insertSpaces } = model.getOptions(); - const eol = model.getEOL(); - const userKeybindingEntries = json.parse(model.getValue()); - const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries); if (userKeybindingEntryIndex !== -1) { // Update the keybinding with new key this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model); } else { - // Add the new keybinidng with new key + // Add the new keybinding with new key this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, keybindingItem.when, false), { tabSize, insertSpaces, eol })[0], model); } - if (keybindingItem.resolvedKeybinding) { - // Unassign the default keybinding - this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model); - } } private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: ITextModel): void { @@ -160,8 +145,8 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding const { tabSize, insertSpaces } = model.getOptions(); const eol = model.getEOL(); const userKeybindingEntries = json.parse(model.getValue()); - const index = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries); - if (index !== -1) { + const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse(); + for (const index of indices) { this.applyEditsToBuffer(setProperty(model.getValue(), [index], void 0, { tabSize, insertSpaces, eol })[0], model); } } @@ -183,13 +168,14 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding return -1; } - private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number { + private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number[] { + const indices = []; for (let index = 0; index < userKeybindingEntries.length; index++) { if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) { - return index; + indices.push(index); } } - return -1; + return indices; } private asObject(key: string, command: string, when: ContextKeyExpr, negate: boolean): any { @@ -255,7 +241,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding private parse(model: ITextModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } { const parseErrors: json.ParseError[] = []; - const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true }); + const result = json.parse(model.getValue(), parseErrors); return { result, parseErrors }; } diff --git a/src/vs/workbench/services/keybinding/common/keybindingIO.ts b/src/vs/workbench/services/keybinding/common/keybindingIO.ts index 13f82fbc817..562266b0926 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingIO.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingIO.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Keybinding, SimpleKeybinding, ChordKeybinding, KeyCodeUtils } from 'vs/base/common/keyCodes'; +import { SimpleKeybinding } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; -import { ScanCodeBinding, ScanCodeUtils } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; export interface IUserKeybindingItem { firstPart: SimpleKeybinding | ScanCodeBinding; @@ -25,12 +26,12 @@ export class KeybindingIO { let quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel()); out.write(`{ "key": ${rightPaddedString(quotedSerializedKeybinding + ',', 25)} "command": `); - let serializedWhen = item.when ? item.when.serialize() : ''; + let quotedSerializedWhen = item.when ? JSON.stringify(item.when.serialize()) : ''; let quotedSerializeCommand = JSON.stringify(item.command); - if (serializedWhen.length > 0) { + if (quotedSerializedWhen.length > 0) { out.write(`${quotedSerializeCommand},`); out.writeLine(); - out.write(` "when": "${serializedWhen}" `); + out.write(` "when": ${quotedSerializedWhen} `); } else { out.write(`${quotedSerializeCommand} `); } @@ -39,7 +40,7 @@ export class KeybindingIO { } public static readUserKeybindingItem(input: IUserFriendlyKeybinding, OS: OperatingSystem): IUserKeybindingItem { - const [firstPart, chordPart] = (typeof input.key === 'string' ? this._readUserBinding(input.key) : [null, null]); + const [firstPart, chordPart] = (typeof input.key === 'string' ? KeybindingParser.parseUserBinding(input.key) : [null, null]); const when = (typeof input.when === 'string' ? ContextKeyExpr.deserialize(input.when) : null); const command = (typeof input.command === 'string' ? input.command : null); const commandArgs = (typeof input.args !== 'undefined' ? input.args : undefined); @@ -51,119 +52,6 @@ export class KeybindingIO { when: when }; } - - private static _readModifiers(input: string) { - input = input.toLowerCase().trim(); - - let ctrl = false; - let shift = false; - let alt = false; - let meta = false; - - let matchedModifier: boolean; - - do { - matchedModifier = false; - if (/^ctrl(\+|\-)/.test(input)) { - ctrl = true; - input = input.substr('ctrl-'.length); - matchedModifier = true; - } - if (/^shift(\+|\-)/.test(input)) { - shift = true; - input = input.substr('shift-'.length); - matchedModifier = true; - } - if (/^alt(\+|\-)/.test(input)) { - alt = true; - input = input.substr('alt-'.length); - matchedModifier = true; - } - if (/^meta(\+|\-)/.test(input)) { - meta = true; - input = input.substr('meta-'.length); - matchedModifier = true; - } - if (/^win(\+|\-)/.test(input)) { - meta = true; - input = input.substr('win-'.length); - matchedModifier = true; - } - if (/^cmd(\+|\-)/.test(input)) { - meta = true; - input = input.substr('cmd-'.length); - matchedModifier = true; - } - } while (matchedModifier); - - let key: string; - - const firstSpaceIdx = input.indexOf(' '); - if (firstSpaceIdx > 0) { - key = input.substring(0, firstSpaceIdx); - input = input.substring(firstSpaceIdx); - } else { - key = input; - input = ''; - } - - return { - remains: input, - ctrl, - shift, - alt, - meta, - key - }; - } - - private static _readSimpleKeybinding(input: string): [SimpleKeybinding, string] { - const mods = this._readModifiers(input); - const keyCode = KeyCodeUtils.fromUserSettings(mods.key); - return [new SimpleKeybinding(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains]; - } - - public static readKeybinding(input: string, OS: OperatingSystem): Keybinding { - if (!input) { - return null; - } - - let [firstPart, remains] = this._readSimpleKeybinding(input); - let chordPart: SimpleKeybinding = null; - if (remains.length > 0) { - [chordPart] = this._readSimpleKeybinding(remains); - } - - if (chordPart) { - return new ChordKeybinding(firstPart, chordPart); - } - return firstPart; - } - - private static _readSimpleUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding, string] { - const mods = this._readModifiers(input); - const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/); - if (scanCodeMatch) { - const strScanCode = scanCodeMatch[1]; - const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode); - return [new ScanCodeBinding(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode), mods.remains]; - } - const keyCode = KeyCodeUtils.fromUserSettings(mods.key); - return [new SimpleKeybinding(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains]; - } - - static _readUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding, SimpleKeybinding | ScanCodeBinding] { - if (!input) { - return [null, null]; - } - - let [firstPart, remains] = this._readSimpleUserBinding(input); - let chordPart: SimpleKeybinding | ScanCodeBinding = null; - if (remains.length > 0) { - [chordPart] = this._readSimpleUserBinding(remains); - } - return [firstPart, chordPart]; - } } function rightPaddedString(str: string, minChars: number): string { diff --git a/src/vs/workbench/services/keybinding/common/keyboardMapper.ts b/src/vs/workbench/services/keybinding/common/keyboardMapper.ts index 0edb5de46fa..ec4e48c6ea1 100644 --- a/src/vs/workbench/services/keybinding/common/keyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/keyboardMapper.ts @@ -7,7 +7,7 @@ import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; -import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; export interface IKeyboardMapper { dumpDebugInfo(): string; diff --git a/src/vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper.ts index f932f7caeec..69720566555 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper.ts @@ -10,7 +10,7 @@ import { ResolvedKeybinding, SimpleKeybinding, Keybinding, KeyCode, ChordKeybind import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ScanCodeBinding, ScanCode, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding, ScanCode, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode'; /** * A keyboard mapper to be used when reading the keymap from the OS fails. diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index 1c56f4f0dde..a077433cb67 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -7,7 +7,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { KeyCode, ResolvedKeybinding, KeyCodeUtils, SimpleKeybinding, Keybinding, KeybindingType, ResolvedKeybindingPart } from 'vs/base/common/keyCodes'; -import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, ScanCodeBinding } from 'vs/base/common/scanCode'; import { CharCode } from 'vs/base/common/charCode'; import { UILabelProvider, AriaLabelProvider, UserSettingsLabelProvider, ElectronAcceleratorLabelProvider } from 'vs/base/common/keybindingLabels'; import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; diff --git a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts index 869932b1b18..9e856dfd8d2 100644 --- a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts @@ -6,7 +6,7 @@ 'use strict'; import { KeyCode, KeyCodeUtils, ResolvedKeybinding, Keybinding, SimpleKeybinding, KeybindingType, ResolvedKeybindingPart } from 'vs/base/common/keyCodes'; -import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, ScanCodeBinding } from 'vs/base/common/scanCode'; import { CharCode } from 'vs/base/common/charCode'; import { UILabelProvider, AriaLabelProvider, ElectronAcceleratorLabelProvider, UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/services/keybinding/electron-browser/keybindingService.ts b/src/vs/workbench/services/keybinding/electron-browser/keybindingService.ts index e7f59e3cd19..8be3cc25d07 100644 --- a/src/vs/workbench/services/keybinding/electron-browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/electron-browser/keybindingService.ts @@ -16,7 +16,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingEvent, IUserFriendlyKeybinding, KeybindingSource, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingItem, KeybindingsRegistry, IKeybindingRule2, KeybindingRuleSource } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IKeybindingItem, KeybindingsRegistry, IKeybindingRule2, KeybindingRuleSource, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { keybindingsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -37,6 +37,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { onUnexpectedError } from 'vs/base/common/errors'; import { release } from 'os'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; export class KeyboardMapperFactory { public static readonly INSTANCE = new KeyboardMapperFactory(); @@ -438,7 +439,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { } public resolveUserBinding(userBinding: string): ResolvedKeybinding[] { - const [firstPart, chordPart] = KeybindingIO._readUserBinding(userBinding); + const [firstPart, chordPart] = KeybindingParser.parseUserBinding(userBinding); return this._keyboardMapper.resolveUserBinding(firstPart, chordPart); } @@ -485,19 +486,19 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { let weight: number; if (isBuiltin) { - weight = KeybindingsRegistry.WEIGHT.builtinExtension(idx); + weight = KeybindingWeight.BuiltinExtension + idx; } else { - weight = KeybindingsRegistry.WEIGHT.externalExtension(idx); + weight = KeybindingWeight.ExternalExtension + idx; } let desc = { id: command, when: ContextKeyExpr.deserialize(when), weight: weight, - primary: KeybindingIO.readKeybinding(key, OS), - mac: mac && { primary: KeybindingIO.readKeybinding(mac, OS) }, - linux: linux && { primary: KeybindingIO.readKeybinding(linux, OS) }, - win: win && { primary: KeybindingIO.readKeybinding(win, OS) } + primary: KeybindingParser.parseKeybinding(key, OS), + mac: mac && { primary: KeybindingParser.parseKeybinding(mac, OS) }, + linux: linux && { primary: KeybindingParser.parseKeybinding(linux, OS) }, + win: win && { primary: KeybindingParser.parseKeybinding(win, OS) } }; if (!desc.primary && !desc.mac && !desc.linux && !desc.win) { @@ -540,6 +541,27 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { let pretty = unboundCommands.sort().join('\n// - '); return '// ' + nls.localize('unboundCommands', "Here are other available commands: ") + '\n// - ' + pretty; } + + mightProducePrintableCharacter(event: IKeyboardEvent): boolean { + if (event.ctrlKey || event.metaKey) { + // ignore ctrl/cmd-combination but not shift/alt-combinatios + return false; + } + // consult the KeyboardMapperFactory to check the given event for + // a printable value. + const mapping = KeyboardMapperFactory.INSTANCE.getRawKeyboardMapping(); + if (!mapping) { + return false; + } + const keyInfo = mapping[event.code]; + if (!keyInfo) { + return false; + } + if (!keyInfo.value || /\s/.test(keyInfo.value)) { + return false; + } + return true; + } } let schemaId = 'vscode://schemas/keybindings'; @@ -585,7 +607,7 @@ const keyboardConfiguration: IConfigurationNode = { 'type': 'string', 'enum': ['code', 'keyCode'], 'default': 'code', - 'description': nls.localize('dispatch', "Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`."), + 'markdownDescription': nls.localize('dispatch', "Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`."), 'included': OS === OperatingSystem.Macintosh || OS === OperatingSystem.Linux }, 'keyboard.touchbar.enabled': { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 2fe4c81b71f..cb922f849d5 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -16,7 +16,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestEditorGroupsService, TestEditorService, TestLogService } from 'vs/workbench/test/workbenchTestServices'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -47,6 +47,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { mkdirp } from 'vs/base/node/pfs'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILogService } from 'vs/platform/log/common/log'; interface Modifiers { metaKey?: boolean; @@ -55,7 +56,7 @@ interface Modifiers { shiftKey?: boolean; } -suite('Keybindings Editing', () => { +suite('KeybindingsEditing', () => { let instantiationService: TestInstantiationService; let testObject: KeybindingsEditingService; @@ -68,7 +69,7 @@ suite('Keybindings Editing', () => { instantiationService = new TestInstantiationService(); - instantiationService.stub(IEnvironmentService, { appKeybindingsPath: keybindingsFile }); + instantiationService.stub(IEnvironmentService, { appKeybindingsPath: keybindingsFile, appSettingsPath: path.join(testDir, 'settings.json') }); instantiationService.stub(IConfigurationService, ConfigurationService); instantiationService.stub(IConfigurationService, 'getValue', { 'eol': '\n' }); instantiationService.stub(IConfigurationService, 'onDidUpdateConfiguration', () => { }); @@ -82,9 +83,10 @@ suite('Keybindings Editing', () => { instantiationService.stub(IEditorService, new TestEditorService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModeService, ModeServiceImpl); + instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); instantiationService.stub(IFileService, new FileService( - new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), + new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), @@ -220,6 +222,21 @@ suite('Keybindings Editing', () => { .then(() => assert.deepEqual(getUserKeybindings(), [])); }); + test('reset mulitple removed keybindings', () => { + writeToKeybindingsFile({ key: 'alt+c', command: '-b' }); + writeToKeybindingsFile({ key: 'alt+shift+c', command: '-b' }); + writeToKeybindingsFile({ key: 'escape', command: '-b' }); + return testObject.resetKeybinding(aResolvedKeybindingItem({ command: 'b', isDefault: false })) + .then(() => assert.deepEqual(getUserKeybindings(), [])); + }); + + test('add a new keybinding to unassigned keybinding', () => { + writeToKeybindingsFile({ key: 'alt+c', command: '-a' }); + const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: '-a' }, { key: 'shift+alt+c', command: 'a' }]; + return testObject.editKeybinding('shift+alt+c', aResolvedKeybindingItem({ command: 'a', isDefault: false })) + .then(() => assert.deepEqual(getUserKeybindings(), expected)); + }); + function writeToKeybindingsFile(...keybindings: IUserFriendlyKeybinding[]) { fs.writeFileSync(keybindingsFile, JSON.stringify(keybindings || [])); } diff --git a/src/vs/workbench/services/keybinding/test/keybindingIO.test.ts b/src/vs/workbench/services/keybinding/test/keybindingIO.test.ts index c3fd1c5d78d..bc0bc9bf481 100644 --- a/src/vs/workbench/services/keybinding/test/keybindingIO.test.ts +++ b/src/vs/workbench/services/keybinding/test/keybindingIO.test.ts @@ -10,7 +10,8 @@ import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybinding import { OS, OperatingSystem } from 'vs/base/common/platform'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ScanCodeBinding, ScanCode } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding, ScanCode } from 'vs/base/common/scanCode'; +import { KeybindingParser } from 'vs/base/common/keybindingParser'; suite('keybindingIO', () => { @@ -28,7 +29,7 @@ suite('keybindingIO', () => { } function testOneDeserialization(keybinding: string, _expected: number, msg: string, OS: OperatingSystem): void { - let actualDeserialized = KeybindingIO.readKeybinding(keybinding, OS); + let actualDeserialized = KeybindingParser.parseKeybinding(keybinding, OS); let expected = createKeybinding(_expected, OS); assert.deepEqual(actualDeserialized, expected, keybinding + ' - ' + msg); } @@ -119,7 +120,7 @@ suite('keybindingIO', () => { test('deserialize scan codes', () => { assert.deepEqual( - KeybindingIO._readUserBinding('ctrl+shift+[comma] ctrl+/'), + KeybindingParser.parseUserBinding('ctrl+shift+[comma] ctrl+/'), [new ScanCodeBinding(true, true, false, false, ScanCode.Comma), new SimpleKeybinding(true, false, false, false, KeyCode.US_SLASH)] ); }); diff --git a/src/vs/workbench/services/keybinding/test/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/keyboardMapperTestUtils.ts index 6cf36eab48f..637f8a9b9b1 100644 --- a/src/vs/workbench/services/keybinding/test/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/keyboardMapperTestUtils.ts @@ -11,7 +11,8 @@ import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common import { TPromise } from 'vs/base/common/winjs.base'; import { readFile, writeFile } from 'vs/base/node/pfs'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; -import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export interface IResolvedKeybinding { label: string; @@ -51,7 +52,7 @@ export function assertResolveUserBinding(mapper: IKeyboardMapper, firstPart: Sim } export function readRawMapping(file: string): TPromise { - return readFile(require.toUrl(`vs/workbench/services/keybinding/test/${file}.js`)).then((buff) => { + return readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/${file}.js`)).then((buff) => { let contents = buff.toString(); let func = new Function('define', contents); let rawMappings: T = null; @@ -63,7 +64,7 @@ export function readRawMapping(file: string): TPromise { } export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): TPromise { - const filePath = require.toUrl(`vs/workbench/services/keybinding/test/${file}`); + const filePath = getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/${file}`); return readFile(filePath).then((buff) => { let expected = buff.toString(); diff --git a/src/vs/workbench/services/keybinding/test/macLinuxFallbackKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/macLinuxFallbackKeyboardMapper.test.ts index 73ff188981b..a3b9c94140f 100644 --- a/src/vs/workbench/services/keybinding/test/macLinuxFallbackKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/macLinuxFallbackKeyboardMapper.test.ts @@ -9,7 +9,7 @@ import { KeyMod, KeyCode, createKeybinding, KeyChord, SimpleKeybinding } from 'v import { OperatingSystem } from 'vs/base/common/platform'; import { IResolvedKeybinding, assertResolveKeybinding, assertResolveKeyboardEvent, assertResolveUserBinding } from 'vs/workbench/services/keybinding/test/keyboardMapperTestUtils'; import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper'; -import { ScanCodeBinding, ScanCode } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding, ScanCode } from 'vs/base/common/scanCode'; suite('keyboardMapper - MAC fallback', () => { diff --git a/src/vs/workbench/services/keybinding/test/macLinuxKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/macLinuxKeyboardMapper.test.ts index 6f414ca6387..63760373f4f 100644 --- a/src/vs/workbench/services/keybinding/test/macLinuxKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/macLinuxKeyboardMapper.test.ts @@ -11,7 +11,7 @@ import { MacLinuxKeyboardMapper, IMacLinuxKeyboardMapping } from 'vs/workbench/s import { OperatingSystem } from 'vs/base/common/platform'; import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ScanCodeUtils, ScanCodeBinding, ScanCode } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeUtils, ScanCodeBinding, ScanCode } from 'vs/base/common/scanCode'; import { TPromise } from 'vs/base/common/winjs.base'; import { readRawMapping, assertMapping, IResolvedKeybinding, assertResolveKeybinding, assertResolveKeyboardEvent, assertResolveUserBinding } from 'vs/workbench/services/keybinding/test/keyboardMapperTestUtils'; diff --git a/src/vs/workbench/services/keybinding/test/windowsKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/windowsKeyboardMapper.test.ts index 85bae2537ac..d5ff4d23d26 100644 --- a/src/vs/workbench/services/keybinding/test/windowsKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/windowsKeyboardMapper.test.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { WindowsKeyboardMapper, IWindowsKeyboardMapping } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper'; import { createKeybinding, KeyMod, KeyCode, KeyChord, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { IResolvedKeybinding, assertResolveKeybinding, readRawMapping, assertMapping, assertResolveKeyboardEvent, assertResolveUserBinding } from 'vs/workbench/services/keybinding/test/keyboardMapperTestUtils'; -import { ScanCodeBinding, ScanCode } from 'vs/workbench/services/keybinding/common/scanCode'; +import { ScanCodeBinding, ScanCode } from 'vs/base/common/scanCode'; const WRITE_FILE_IF_DIFFERENT = false; diff --git a/src/vs/workbench/services/mode/common/workbenchModeService.ts b/src/vs/workbench/services/mode/common/workbenchModeService.ts index 9b2fa23f592..5272f83c9e6 100644 --- a/src/vs/workbench/services/mode/common/workbenchModeService.ts +++ b/src/vs/workbench/services/mode/common/workbenchModeService.ts @@ -5,20 +5,31 @@ 'use strict'; import * as nls from 'vs/nls'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import * as paths from 'vs/base/common/paths'; +import * as resources from 'vs/base/common/resources'; import { TPromise } from 'vs/base/common/winjs.base'; import * as mime from 'vs/base/common/mime'; import { IFilesConfiguration, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; -import { ILanguageExtensionPoint, IValidLanguageExtensionPoint } from 'vs/editor/common/services/modeService'; +import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { URI } from 'vs/base/common/uri'; -export const languagesExtPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint('languages', [], { +export interface IRawLanguageExtensionPoint { + id: string; + extensions: string[]; + filenames: string[]; + filenamePatterns: string[]; + firstLine: string; + aliases: string[]; + mimetypes: string[]; + configuration: string; +} + +export const languagesExtPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint('languages', [], { description: nls.localize('vscode.extension.contributes.languages', 'Contributes language declarations.'), type: 'array', items: { @@ -92,8 +103,8 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl { this._configurationService = configurationService; this._extensionService = extensionService; - languagesExtPoint.setHandler((extensions: IExtensionPointUser[]) => { - let allValidLanguages: IValidLanguageExtensionPoint[] = []; + languagesExtPoint.setHandler((extensions: IExtensionPointUser[]) => { + let allValidLanguages: ILanguageExtensionPoint[] = []; for (let i = 0, len = extensions.length; i < len; i++) { let extension = extensions[i]; @@ -106,8 +117,10 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl { for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) { let ext = extension.value[j]; if (isValidLanguageExtensionPoint(ext, extension.collector)) { - // TODO@extensionLocation - let configuration = (ext.configuration ? paths.join(extension.description.extensionLocation.fsPath, ext.configuration) : ext.configuration); + let configuration: URI; + if (ext.configuration) { + configuration = resources.joinPath(extension.description.extensionLocation, ext.configuration); + } allValidLanguages.push({ id: ext.id, extensions: ext.extensions, @@ -133,7 +146,7 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl { }); this.onDidCreateMode((mode) => { - this._extensionService.activateByEvent(`onLanguage:${mode.getId()}`).done(null, onUnexpectedError); + this._extensionService.activateByEvent(`onLanguage:${mode.getId()}`); }); } @@ -176,7 +189,7 @@ function isUndefinedOrStringArray(value: string[]): boolean { return value.every(item => typeof item === 'string'); } -function isValidLanguageExtensionPoint(value: ILanguageExtensionPoint, collector: ExtensionMessageCollector): boolean { +function isValidLanguageExtensionPoint(value: IRawLanguageExtensionPoint, collector: ExtensionMessageCollector): boolean { if (!value) { collector.error(nls.localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name)); return false; diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 52fbfe0638c..bd0daafa022 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -7,31 +7,22 @@ import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification'; import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; import { once } from 'vs/base/common/event'; -export class NotificationService implements INotificationService { +export class NotificationService extends Disposable implements INotificationService { - public _serviceBrand: any; + _serviceBrand: any; - private _model: INotificationsModel; - private toDispose: IDisposable[]; + private _model: INotificationsModel = this._register(new NotificationsModel()); - constructor() { - this.toDispose = []; - - const model = new NotificationsModel(); - this.toDispose.push(model); - this._model = model; - } - - public get model(): INotificationsModel { + get model(): INotificationsModel { return this._model; } - public info(message: NotificationMessage | NotificationMessage[]): void { + info(message: NotificationMessage | NotificationMessage[]): void { if (Array.isArray(message)) { message.forEach(m => this.info(m)); @@ -41,7 +32,7 @@ export class NotificationService implements INotificationService { this.model.notify({ severity: Severity.Info, message }); } - public warn(message: NotificationMessage | NotificationMessage[]): void { + warn(message: NotificationMessage | NotificationMessage[]): void { if (Array.isArray(message)) { message.forEach(m => this.warn(m)); @@ -51,7 +42,7 @@ export class NotificationService implements INotificationService { this.model.notify({ severity: Severity.Warning, message }); } - public error(message: NotificationMessage | NotificationMessage[]): void { + error(message: NotificationMessage | NotificationMessage[]): void { if (Array.isArray(message)) { message.forEach(m => this.error(m)); @@ -61,11 +52,11 @@ export class NotificationService implements INotificationService { this.model.notify({ severity: Severity.Error, message }); } - public notify(notification: INotification): INotificationHandle { + notify(notification: INotification): INotificationHandle { return this.model.notify(notification); } - public prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { + prompt(severity: Severity, message: string, choices: IPromptChoice[], onCancel?: () => void): INotificationHandle { let handle: INotificationHandle; let choiceClicked = false; @@ -109,8 +100,4 @@ export class NotificationService implements INotificationService { return handle; } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } } \ No newline at end of file diff --git a/src/vs/workbench/services/part/common/partService.ts b/src/vs/workbench/services/part/common/partService.ts index c96d2e0ed70..a768e4c160a 100644 --- a/src/vs/workbench/services/part/common/partService.ts +++ b/src/vs/workbench/services/part/common/partService.ts @@ -7,21 +7,30 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; +import { MenuBarVisibility } from 'vs/platform/windows/common/windows'; -export enum Parts { +export const enum Parts { ACTIVITYBAR_PART, SIDEBAR_PART, PANEL_PART, EDITOR_PART, STATUSBAR_PART, - TITLEBAR_PART + TITLEBAR_PART, + MENUBAR_PART } -export enum Position { +export const enum Position { LEFT, RIGHT, BOTTOM } +export function PositionToString(position: Position): string { + switch (position) { + case Position.LEFT: return 'LEFT'; + case Position.RIGHT: return 'RIGHT'; + case Position.BOTTOM: return 'BOTTOM'; + } +} export interface ILayoutOptions { toggleMaximizedPanel?: boolean; @@ -48,11 +57,6 @@ export interface IPartService { */ onEditorLayout: Event; - /** - * Asks the part service to layout all parts. - */ - layout(options?: ILayoutOptions): void; - /** * Asks the part service to if all parts have been created. */ @@ -109,6 +113,11 @@ export interface IPartService { */ getSideBarPosition(): Position; + /** + * Gets the current menubar visibility. + */ + getMenubarVisibility(): MenuBarVisibility; + /** * Gets the current panel position. Note that the panel can be hidden too. */ @@ -120,9 +129,9 @@ export interface IPartService { setPanelPosition(position: Position): TPromise; /** - * Returns the identifier of the element that contains the workbench. + * Returns the element that contains the workbench. */ - getWorkbenchElementId(): string; + getWorkbenchElement(): HTMLElement; /** * Toggles the workbench in and out of zen mode - parts get hidden and window goes fullscreen. diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index c9d36e8cbf7..5de3ae783e9 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -3,40 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as network from 'vs/base/common/network'; -import { TPromise } from 'vs/base/common/winjs.base'; -import * as nls from 'vs/nls'; -import URI from 'vs/base/common/uri'; -import * as labels from 'vs/base/common/labels'; -import * as strings from 'vs/base/common/strings'; -import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, IEditor } from 'vs/workbench/common/editor'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { ITextModel } from 'vs/editor/common/model'; -import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { parse } from 'vs/base/common/json'; -import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as network from 'vs/base/common/network'; import { assign } from 'vs/base/common/objects'; +import * as strings from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import * as nls from 'vs/nls'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { EditorInput, IEditor } from 'vs/workbench/common/editor'; +import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroup, IEditorGroupsService, GroupDirection } from 'vs/workbench/services/group/common/editorGroupsService'; +import { GroupDirection, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTargetName, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, SettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; +import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; +import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; const emptyEditableSettingsContent = '{\n}'; @@ -69,7 +69,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IKeybindingService keybindingService: IKeybindingService, @IModelService private modelService: IModelService, @IJSONEditingService private jsonEditingService: IJSONEditingService, - @IModeService private modeService: IModeService + @IModeService private modeService: IModeService, + @ILabelService private labelService: ILabelService ) { super(); // The default keybindings.json updates based on keyboard layouts, so here we make sure @@ -95,6 +96,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); } + get settingsEditor2Input(): SettingsEditor2Input { + return this.instantiationService.createInstance(SettingsEditor2Input); + } + getFolderSettingsResource(resource: URI): URI { return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, resource); } @@ -115,7 +120,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return; } defaultSettings = this.getDefaultSettings(target); - this.modelService.updateModel(model, defaultSettings.parse()); + this.modelService.updateModel(model, defaultSettings.getContent(true)); defaultSettings._onDidChange.fire(); } }); @@ -123,7 +128,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic // Check if Default settings is already created and updated in above promise if (!defaultSettings) { defaultSettings = this.getDefaultSettings(target); - this.modelService.updateModel(model, defaultSettings.parse()); + this.modelService.updateModel(model, defaultSettings.getContent(true)); } return TPromise.as(model); @@ -175,34 +180,67 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorService.openEditor({ resource: this.userSettingsResource }); } - openSettings(): TPromise { + openSettings(jsonEditor?: boolean): TPromise { + jsonEditor = typeof jsonEditor === 'undefined' ? + this.configurationService.getValue('workbench.settings.editor') === 'json' : + jsonEditor; + + if (!jsonEditor) { + return this.openSettings2(); + } + const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput; const resource = editorInput ? editorInput.master.getResource() : this.userSettingsResource; const target = this.getConfigurationTargetFromSettingsResource(resource); return this.openOrSwitchSettings(target, resource); } - openGlobalSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise { - return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, group); + private openSettings2(): TPromise { + const input = this.settingsEditor2Input; + return this.editorGroupService.activeGroup.openEditor(input) + .then(() => this.editorGroupService.activeGroup.activeControl); } - openSettings2(): TPromise { - return this.editorService.openEditor(this.instantiationService.createInstance(SettingsEditor2Input), { pinned: true }).then(() => null); + openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise { + jsonEditor = typeof jsonEditor === 'undefined' ? + this.configurationService.getValue('workbench.settings.editor') === 'json' : + jsonEditor; + + return jsonEditor ? + this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, group) : + this.openOrSwitchSettings2(ConfigurationTarget.USER, undefined, options, group); } - openWorkspaceSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise { + openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise { + jsonEditor = typeof jsonEditor === 'undefined' ? + this.configurationService.getValue('workbench.settings.editor') === 'json' : + jsonEditor; + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.notificationService.info(nls.localize('openFolderFirst', "Open a folder first to create workspace settings")); return TPromise.as(null); } - return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, group); + + return jsonEditor ? + this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, group) : + this.openOrSwitchSettings2(ConfigurationTarget.WORKSPACE, undefined, options, group); } - openFolderSettings(folder: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise { - return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, group); + openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise { + jsonEditor = typeof jsonEditor === 'undefined' ? + this.configurationService.getValue('workbench.settings.editor') === 'json' : + jsonEditor; + + return jsonEditor ? + this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, group) : + this.openOrSwitchSettings2(ConfigurationTarget.WORKSPACE_FOLDER, folder, options, group); } - switchSettings(target: ConfigurationTarget, resource: URI): TPromise { + switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): TPromise { + if (!jsonEditor) { + return this.doOpenSettings2(target, resource).then(() => null); + } + const activeControl = this.editorService.activeControl; if (activeControl && activeControl.input instanceof PreferencesEditorInput) { return this.doSwitchSettings(target, resource, activeControl.input, activeControl.group).then(() => null); @@ -221,44 +259,60 @@ export class PreferencesService extends Disposable implements IPreferencesServic if (textual) { const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + '\n[\n]'; const editableKeybindings = URI.file(this.environmentService.appKeybindingsPath); + const openDefaultKeybindings = !!this.configurationService.getValue('workbench.settings.openDefaultKeybindings'); // Create as needed and open in editor return this.createIfNotExists(editableKeybindings, emptyContents).then(() => { - const activeEditorGroup = this.editorGroupService.activeGroup; - const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT); - - return TPromise.join([ - this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }), - this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true } }, sideEditorGroup.id) - ]).then(editors => void 0); + if (openDefaultKeybindings) { + const activeEditorGroup = this.editorGroupService.activeGroup; + const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT); + return TPromise.join([ + this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }), + this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true } }, sideEditorGroup.id) + ]).then(editors => void 0); + } else { + return this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true } }).then(() => void 0); + } }); } + return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true }).then(() => null); } + openDefaultKeybindingsFile(): TPromise { + return this.editorService.openEditor({ resource: this.defaultKeybindingsResource }); + } + configureSettingsForLanguage(language: string): void { this.openGlobalSettings() - .then(editor => { - const codeEditor = getCodeEditor(editor.getControl()); - if (codeEditor) { - this.getPosition(language, codeEditor) - .then(position => { - codeEditor.setPosition(position); - codeEditor.focus(); - }); - } - }); + .then(editor => this.createPreferencesEditorModel(this.userSettingsResource) + .then((settingsModel: IPreferencesEditorModel) => { + const codeEditor = getCodeEditor(editor.getControl()); + if (codeEditor) { + this.getPosition(language, settingsModel, codeEditor) + .then(position => { + if (codeEditor) { + codeEditor.setPosition(position); + codeEditor.focus(); + } + }); + } + })); } - private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): TPromise { + private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): TPromise { const editorInput = this.getActiveSettingsEditorInput(group); if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) { - return this.doSwitchSettings(configurationTarget, resource, editorInput, group); + return this.doSwitchSettings(configurationTarget, resource, editorInput, group, options); } return this.doOpenSettings(configurationTarget, resource, options, group); } - private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise { + private openOrSwitchSettings2(configurationTarget: ConfigurationTarget, folderUri?: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): TPromise { + return this.doOpenSettings2(configurationTarget, folderUri, options, group); + } + + private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise { const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING); return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource) .then(editableSettingsEditorInput => { @@ -272,23 +326,41 @@ export class PreferencesService extends Disposable implements IPreferencesServic const defaultPreferencesEditorInput = this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(configurationTarget)); const preferencesEditorInput = new PreferencesEditorInput(this.getPreferencesEditorInputName(configurationTarget, resource), editableSettingsEditorInput.getDescription(), defaultPreferencesEditorInput, editableSettingsEditorInput); this.lastOpenedSettingsInput = preferencesEditorInput; - return this.editorService.openEditor(preferencesEditorInput, options, group); + return this.editorService.openEditor(preferencesEditorInput, SettingsEditorOptions.create(options), group); } - return this.editorService.openEditor(editableSettingsEditorInput, options, group); + return this.editorService.openEditor(editableSettingsEditorInput, SettingsEditorOptions.create(options), group); }); } - private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, group: IEditorGroup): TPromise { + public createSettings2EditorModel(): Settings2EditorModel { + return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER)); + } + + private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): TPromise { + const input = this.settingsEditor2Input; + const settingsOptions: ISettingsEditorOptions = { + ...options, + target, + folderUri + }; + + return this.editorService.openEditor(input, SettingsEditorOptions.create(settingsOptions), group); + } + + private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, group: IEditorGroup, options?: ISettingsEditorOptions): TPromise { return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource)) .then(toInput => { - const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput); + return group.openEditor(input).then(() => { + const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput); - return group.replaceEditors([{ - editor: input, - replacement: replaceWith - }]).then(() => { - this.lastOpenedSettingsInput = replaceWith; - return group.activeControl; + return group.replaceEditors([{ + editor: input, + replacement: replaceWith, + options: SettingsEditorOptions.create(options) + }]).then(() => { + this.lastOpenedSettingsInput = replaceWith; + return group.activeControl; + }); }); }); } @@ -430,7 +502,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(null, error => { if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { return this.fileService.updateContent(resource, contents).then(null, error => { - return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", labels.getPathLabel(resource, this.contextService, this.environmentService), error))); + return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", this.labelService.getUriLabel(resource, true), error))); }); } @@ -454,39 +526,36 @@ export class PreferencesService extends Disposable implements IPreferencesServic ]; } - private getPosition(language: string, codeEditor: ICodeEditor): TPromise { - return this.createPreferencesEditorModel(this.userSettingsResource) - .then((settingsModel: IPreferencesEditorModel) => { - const languageKey = `[${language}]`; - let setting = settingsModel.getPreference(languageKey); - const model = codeEditor.getModel(); - const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>(); - const eol = configuration.files && configuration.files.eol; - if (setting) { - if (setting.overrides.length) { - const lastSetting = setting.overrides[setting.overrides.length - 1]; - let content; - if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) { - content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor); - } else { - content = ',' + eol + this.spaces(2, configuration.editor); - } - const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content); - model.pushEditOperations([], [editOperation], () => []); - return { lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) }; - } - return { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 }; + private getPosition(language: string, settingsModel: IPreferencesEditorModel, codeEditor: ICodeEditor): TPromise { + const languageKey = `[${language}]`; + let setting = settingsModel.getPreference(languageKey); + const model = codeEditor.getModel(); + const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>(); + const eol = configuration.files && configuration.files.eol; + if (setting) { + if (setting.overrides.length) { + const lastSetting = setting.overrides[setting.overrides.length - 1]; + let content; + if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) { + content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor); + } else { + content = ',' + eol + this.spaces(2, configuration.editor); } - return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER) - .then(() => { - setting = settingsModel.getPreference(languageKey); - let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor); - let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content); - model.pushEditOperations([], [editOperation], () => []); - let lineNumber = setting.valueRange.endLineNumber + 1; - settingsModel.dispose(); - return { lineNumber, column: model.getLineMaxColumn(lineNumber) }; - }); + const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content); + model.pushEditOperations([], [editOperation], () => []); + return TPromise.as({ lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) }); + } + return TPromise.as({ lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 }); + } + return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER) + .then(() => { + setting = settingsModel.getPreference(languageKey); + let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor); + let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content); + model.pushEditOperations([], [editOperation], () => []); + let lineNumber = setting.valueRange.endLineNumber + 1; + settingsModel.dispose(); + return { lineNumber, column: model.getLineMaxColumn(lineNumber) }; }); } diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 82028544ee6..baa3489c3f7 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditor } from 'vs/workbench/common/editor'; +import { IEditor, EditorOptions } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; import { IRange } from 'vs/editor/common/core/range'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -18,6 +18,8 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { localize } from 'vs/nls'; import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; +import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; export interface ISettingsGroup { id: string; @@ -25,6 +27,7 @@ export interface ISettingsGroup { title: string; titleRange: IRange; sections: ISettingsSection[]; + contributedByExtension: boolean; } export interface ISettingsSection { @@ -40,13 +43,19 @@ export interface ISetting { value: any; valueRange: IRange; description: string[]; + descriptionIsMarkdown: boolean; descriptionRanges: IRange[]; overrides?: ISetting[]; overrideOf?: ISetting; + deprecationMessage?: string; - // TODO@roblou maybe need new type and new EditorModel for GUI editor instead of ISetting which is used for text settings editor + scope?: ConfigurationScope; type?: string | string[]; enum?: string[]; + enumDescriptions?: string[]; + enumDescriptionsAreMarkdown?: boolean; + tags?: string[]; + validator?: (value: any) => string; } export interface IExtensionSetting extends ISetting { @@ -56,6 +65,7 @@ export interface IExtensionSetting extends ISetting { export interface ISearchResult { filterMatches: ISettingMatch[]; + exactMatch?: boolean; metadata?: IFilterMetadata; } @@ -72,6 +82,7 @@ export interface IFilterResult { allGroups: ISettingsGroup[]; matches: IRange[]; metadata?: IStringDictionary; + exactMatch?: boolean; } export interface ISettingMatch { @@ -111,7 +122,7 @@ export interface IFilterMetadata { } export interface IPreferencesEditorModel { - uri: URI; + uri?: URI; getPreference(key: string): T; dispose(): void; } @@ -127,6 +138,45 @@ export interface ISettingsEditorModel extends IPreferencesEditorModel updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult; } +export interface ISettingsEditorOptions extends IEditorOptions { + target?: ConfigurationTarget; + folderUri?: URI; + query?: string; +} + +/** + * TODO Why do we need this class? + */ +export class SettingsEditorOptions extends EditorOptions implements ISettingsEditorOptions { + + target?: ConfigurationTarget; + folderUri?: URI; + query?: string; + + static create(settings: ISettingsEditorOptions): SettingsEditorOptions { + if (!settings) { + return null; + } + + const options = new SettingsEditorOptions(); + + options.target = settings.target; + options.folderUri = settings.folderUri; + options.query = settings.query; + + // IEditorOptions + options.preserveFocus = settings.preserveFocus; + options.forceReload = settings.forceReload; + options.revealIfVisible = settings.revealIfVisible; + options.revealIfOpened = settings.revealIfOpened; + options.pinned = settings.pinned; + options.index = settings.index; + options.inactive = settings.inactive; + + return options; + } +} + export interface IKeybindingsEditorModel extends IPreferencesEditorModel { } @@ -141,15 +191,16 @@ export interface IPreferencesService { resolveModel(uri: URI): TPromise; createPreferencesEditorModel(uri: URI): TPromise>; + createSettings2EditorModel(): Settings2EditorModel; // TODO openRawDefaultSettings(): TPromise; - openSettings(): TPromise; - openSettings2(): TPromise; - openGlobalSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise; - openWorkspaceSettings(options?: IEditorOptions, group?: IEditorGroup): TPromise; - openFolderSettings(folder: URI, options?: IEditorOptions, group?: IEditorGroup): TPromise; - switchSettings(target: ConfigurationTarget, resource: URI): TPromise; + openSettings(jsonEditor?: boolean): TPromise; + openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise; + openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise; + openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): TPromise; + switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): TPromise; openGlobalKeybindingSettings(textual: boolean): TPromise; + openDefaultKeybindingsFile(): TPromise; configureSettingsForLanguage(language: string): void; } diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 6fb507a0656..4bd8f3110af 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OS } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; @@ -13,8 +13,8 @@ import { EditorInput, SideBySideEditorInput, Verbosity } from 'vs/workbench/comm import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; -import { IPreferencesService } from './preferences'; -import { DefaultSettingsEditorModel } from './preferencesModels'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; export class PreferencesEditorInput extends SideBySideEditorInput { public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput'; @@ -23,10 +23,6 @@ export class PreferencesEditorInput extends SideBySideEditorInput { return PreferencesEditorInput.ID; } - public supportsSplitEditor(): boolean { - return true; - } - public getTitle(verbosity: Verbosity): string { return this.master.getTitle(verbosity); } @@ -74,7 +70,7 @@ export class KeybindingsEditorInput extends EditorInput { return nls.localize('keybindingsInputName', "Keyboard Shortcuts"); } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(this.keybindingsModel); } @@ -86,11 +82,18 @@ export class KeybindingsEditorInput extends EditorInput { export class SettingsEditor2Input extends EditorInput { public static readonly ID: string = 'workbench.input.settings2'; + private readonly _settingsModel: Settings2EditorModel; constructor( - @IPreferencesService private preferencesService: IPreferencesService + @IPreferencesService _preferencesService: IPreferencesService, ) { super(); + + this._settingsModel = _preferencesService.createSettings2EditorModel(); + } + + matches(otherInput: any): boolean { + return otherInput instanceof SettingsEditor2Input; } getTypeId(): string { @@ -98,14 +101,17 @@ export class SettingsEditor2Input extends EditorInput { } getName(): string { - return nls.localize('settingsEditor2InputName', "Settings (Preview)"); + return nls.localize('settingsEditor2InputName', "Settings"); } - resolve(refresh?: boolean): TPromise { - return >this.preferencesService.createPreferencesEditorModel(URI.parse('vscode://defaultsettings/0/settings.json')); + resolve(): TPromise { + return TPromise.as(this._settingsModel); } - matches(otherInput: any): boolean { - return otherInput instanceof SettingsEditor2Input; + public getResource(): URI { + return URI.from({ + scheme: 'vscode-settings', + path: `settingseditor` + }); } } diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index a20abf46e45..5ebf357c101 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten, tail } from 'vs/base/common/arrays'; +import { flatten, tail, find } from 'vs/base/common/arrays'; import { IStringDictionary } from 'vs/base/common/collections'; import { Emitter, Event } from 'vs/base/common/event'; import { JSONVisitor, visit } from 'vs/base/common/json'; import { Disposable, IReference } from 'vs/base/common/lifecycle'; import * as map from 'vs/base/common/map'; import { assign } from 'vs/base/common/objects'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -189,7 +189,8 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti settings: filteredSettings }], title: modelGroup.title, - titleRange: modelGroup.titleRange + titleRange: modelGroup.titleRange, + contributedByExtension: !!modelGroup.contributedByExtension }; } @@ -203,6 +204,47 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti } } +export class Settings2EditorModel extends AbstractSettingsModel implements ISettingsEditorModel { + private readonly _onDidChangeGroups: Emitter = this._register(new Emitter()); + readonly onDidChangeGroups: Event = this._onDidChangeGroups.event; + + private dirty = false; + + constructor( + private _defaultSettings: DefaultSettings, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + configurationService.onDidChangeConfiguration(e => { + if (e.source === ConfigurationTarget.DEFAULT) { + this.dirty = true; + this._onDidChangeGroups.fire(); + } + }); + } + + protected get filterGroups(): ISettingsGroup[] { + // Don't filter "commonly used" + return this.settingsGroups.slice(1); + } + + public get settingsGroups(): ISettingsGroup[] { + const groups = this._defaultSettings.getSettingsGroups(this.dirty); + this.dirty = false; + return groups; + } + + public findValueMatches(filter: string, setting: ISetting): IRange[] { + // TODO @roblou + return []; + } + + protected update(): IFilterResult { + throw new Error('Not supported'); + } +} + function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, previousParents: string[]) => boolean): ISettingsGroup[] { const settings: ISetting[] = []; let overrideSetting: ISetting = null; @@ -266,6 +308,7 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, let settingStartPosition = model.getPositionAt(offset); const setting: ISetting = { description: [], + descriptionIsMarkdown: false, key: name, keyRange: { startLineNumber: settingStartPosition.lineNumber, @@ -408,45 +451,53 @@ export class DefaultSettings extends Disposable { super(); } - get content(): string { - if (!this._content) { - this.parse(); + getContent(forceUpdate = false): string { + if (!this._content || forceUpdate) { + this._content = this.toContent(true, this.getSettingsGroups(forceUpdate)); } + return this._content; } - get settingsGroups(): ISettingsGroup[] { - if (!this._allSettingsGroups) { - this.parse(); + getSettingsGroups(forceUpdate = false): ISettingsGroup[] { + if (!this._allSettingsGroups || forceUpdate) { + this._allSettingsGroups = this.parse(); } return this._allSettingsGroups; } - parse(): string { + private parse(): ISettingsGroup[] { const settingsGroups = this.getRegisteredGroups(); this.initAllSettingsMap(settingsGroups); const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups); - this._allSettingsGroups = [mostCommonlyUsed, ...settingsGroups]; - this._content = this.toContent(true, this._allSettingsGroups); - return this._content; + return [mostCommonlyUsed, ...settingsGroups]; } get raw(): string { if (!DefaultSettings._RAW) { DefaultSettings._RAW = this.toContent(false, this.getRegisteredGroups()); } - return DefaultSettings._RAW; - } - getSettingByName(name: string): ISetting { - return this._settingsByName && this._settingsByName.get(name); + return DefaultSettings._RAW; } private getRegisteredGroups(): ISettingsGroup[] { const configurations = Registry.as(Extensions.Configuration).getConfigurations().slice(); - return this.removeEmptySettingsGroups(configurations.sort(this.compareConfigurationNodes) + const groups = this.removeEmptySettingsGroups(configurations.sort(this.compareConfigurationNodes) .reduce((result, config, index, array) => this.parseConfig(config, result, array), [])); + + return this.sortGroups(groups); + } + + private sortGroups(groups: ISettingsGroup[]): ISettingsGroup[] { + groups.forEach(group => { + group.sections.forEach(section => { + section.settings.sort((a, b) => a.key.localeCompare(b.key)); + }); + }); + + return groups; } private initAllSettingsMap(allSettingsGroups: ISettingsGroup[]): void { @@ -471,8 +522,10 @@ export class DefaultSettings extends Disposable { range: null, valueRange: null, overrides: [], + scope: ConfigurationScope.RESOURCE, type: setting.type, - enum: setting.enum + enum: setting.enum, + enumDescriptions: setting.enumDescriptions }; } return null; @@ -491,19 +544,20 @@ export class DefaultSettings extends Disposable { }; } - private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup): ISettingsGroup[] { + private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup, seenSettings?: { [key: string]: boolean }): ISettingsGroup[] { + seenSettings = seenSettings ? seenSettings : {}; let title = config.title; if (!title) { - const configWithTitleAndSameId = configurations.filter(c => c.id === config.id && c.title)[0]; + const configWithTitleAndSameId = find(configurations, c => (c.id === config.id) && c.title); if (configWithTitleAndSameId) { title = configWithTitleAndSameId.title; } } if (title) { if (!settingsGroup) { - settingsGroup = result.filter(g => g.title === title)[0]; + settingsGroup = find(result, g => g.title === title); if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null }; + settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null, contributedByExtension: !!config.contributedByExtension }; result.push(settingsGroup); } } else { @@ -512,17 +566,22 @@ export class DefaultSettings extends Disposable { } if (config.properties) { if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null }; + settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null, contributedByExtension: !!config.contributedByExtension }; result.push(settingsGroup); } - const configurationSettings: ISetting[] = [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]; + const configurationSettings: ISetting[] = []; + for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)]) { + if (!seenSettings[setting.key]) { + configurationSettings.push(setting); + seenSettings[setting.key] = true; + } + } if (configurationSettings.length) { - configurationSettings.sort((a, b) => a.key.localeCompare(b.key)); settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings; } } if (config.allOf) { - config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup)); + config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup, seenSettings)); } return result; } @@ -542,18 +601,46 @@ export class DefaultSettings extends Disposable { let result: ISetting[] = []; for (let key in settingsObject) { const prop = settingsObject[key]; - if (!prop.deprecationMessage && this.matchesScope(prop)) { + if (this.matchesScope(prop)) { const value = prop.default; - const description = (prop.description || '').split('\n'); + const description = (prop.description || prop.markdownDescription || '').split('\n'); const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : []; - result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides, type: prop.type, enum: prop.enum }); + result.push({ + key, + value, + description, + descriptionIsMarkdown: !prop.description, + range: null, + keyRange: null, + valueRange: null, + descriptionRanges: [], + overrides, + scope: prop.scope, + type: prop.type, + enum: prop.enum, + enumDescriptions: prop.enumDescriptions || prop.markdownEnumDescriptions, + enumDescriptionsAreMarkdown: !prop.enumDescriptions, + tags: prop.tags, + deprecationMessage: prop.deprecationMessage, + validator: createValidator(prop) + }); } } return result; } private parseOverrideSettings(overrideSettings: any): ISetting[] { - return Object.keys(overrideSettings).map((key) => ({ key, value: overrideSettings[key], description: [], range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides: [] })); + return Object.keys(overrideSettings).map((key) => ({ + key, + value: overrideSettings[key], + description: [], + descriptionIsMarkdown: false, + range: null, + keyRange: null, + valueRange: null, + descriptionRanges: [], + overrides: [] + })); } private matchesScope(property: IConfigurationNode): boolean { @@ -626,7 +713,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements } public get settingsGroups(): ISettingsGroup[] { - return this.defaultSettings.settingsGroups; + return this.defaultSettings.getSettingsGroups(); } protected get filterGroups(): ISettingsGroup[] { @@ -730,13 +817,23 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements } private copySetting(setting: ISetting): ISetting { - return { + return { description: setting.description, + scope: setting.scope, + type: setting.type, + enum: setting.enum, + enumDescriptions: setting.enumDescriptions, key: setting.key, value: setting.value, range: setting.range, overrides: [], - overrideOf: setting.overrideOf + overrideOf: setting.overrideOf, + tags: setting.tags, + deprecationMessage: setting.deprecationMessage, + keyRange: undefined, + valueRange: undefined, + descriptionIsMarkdown: undefined, + descriptionRanges: undefined }; } @@ -840,26 +937,48 @@ class SettingsContentBuilder { private pushSetting(setting: ISetting, indent: string): void { const settingStart = this.lineCountWithOffset + 1; + + this.pushSettingDescription(setting, indent); + + let preValueContent = indent; + const keyString = JSON.stringify(setting.key); + preValueContent += keyString; + setting.keyRange = { startLineNumber: this.lineCountWithOffset + 1, startColumn: preValueContent.indexOf(setting.key) + 1, endLineNumber: this.lineCountWithOffset + 1, endColumn: setting.key.length }; + + preValueContent += ': '; + const valueStart = this.lineCountWithOffset + 1; + this.pushValue(setting, preValueContent, indent); + + setting.valueRange = { startLineNumber: valueStart, startColumn: preValueContent.length + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length + 1 }; + this._contentByLines[this._contentByLines.length - 1] += ','; + this._contentByLines.push(''); + setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; + } + + private pushSettingDescription(setting: ISetting, indent: string): void { + const fixSettingLink = line => line.replace(/`#(.*)#`/g, (match, settingName) => `\`${settingName}\``); + setting.descriptionRanges = []; const descriptionPreValue = indent + '// '; - for (const line of setting.description) { + for (let line of (setting.deprecationMessage ? [setting.deprecationMessage, ...setting.description] : setting.description)) { + line = fixSettingLink(line); + this._contentByLines.push(descriptionPreValue + line); setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); } - let preValueConent = indent; - const keyString = JSON.stringify(setting.key); - preValueConent += keyString; - setting.keyRange = { startLineNumber: this.lineCountWithOffset + 1, startColumn: preValueConent.indexOf(setting.key) + 1, endLineNumber: this.lineCountWithOffset + 1, endColumn: setting.key.length }; + if (setting.enumDescriptions && setting.enumDescriptions.some(desc => !!desc)) { + setting.enumDescriptions.forEach((desc, i) => { + const displayEnum = escapeInvisibleChars(String(setting.enum[i])); + const line = desc ? + `${displayEnum}: ${fixSettingLink(desc)}` : + displayEnum; - preValueConent += ': '; - const valueStart = this.lineCountWithOffset + 1; - this.pushValue(setting, preValueConent, indent); + this._contentByLines.push(` // - ${line}`); - setting.valueRange = { startLineNumber: valueStart, startColumn: preValueConent.length + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length + 1 }; - this._contentByLines[this._contentByLines.length - 1] += ','; - this._contentByLines.push(''); - setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; + setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); + }); + } } private pushValue(setting: ISetting, preValueConent: string, indent: string): void { @@ -894,6 +1013,118 @@ class SettingsContentBuilder { } } +export function createValidator(prop: IConfigurationPropertySchema): ((value: any) => string) | null { + return value => { + let exclusiveMax: number | undefined; + let exclusiveMin: number | undefined; + + if (typeof prop.exclusiveMaximum === 'boolean') { + exclusiveMax = prop.exclusiveMaximum ? prop.maximum : undefined; + } else { + exclusiveMax = prop.exclusiveMaximum; + } + + if (typeof prop.exclusiveMinimum === 'boolean') { + exclusiveMin = prop.exclusiveMinimum ? prop.minimum : undefined; + } else { + exclusiveMin = prop.exclusiveMinimum; + } + + let patternRegex: RegExp | undefined; + if (typeof prop.pattern === 'string') { + patternRegex = new RegExp(prop.pattern); + } + + const type = Array.isArray(prop.type) ? prop.type : [prop.type]; + const canBeType = (t: string) => type.indexOf(t) > -1; + + const isNullable = canBeType('null'); + const isNumeric = (canBeType('number') || canBeType('integer')) && (type.length === 1 || type.length === 2 && isNullable); + const isIntegral = (canBeType('integer')) && (type.length === 1 || type.length === 2 && isNullable); + + type Validator = { enabled: boolean, isValid: (value: T) => boolean; message: string }; + + let numericValidations: Validator[] = isNumeric ? [ + { + enabled: exclusiveMax !== undefined && (prop.maximum === undefined || exclusiveMax <= prop.maximum), + isValid: (value => value < exclusiveMax), + message: nls.localize('validations.exclusiveMax', "Value must be strictly less than {0}.", exclusiveMax) + }, + { + enabled: exclusiveMin !== undefined && (prop.minimum === undefined || exclusiveMin >= prop.minimum), + isValid: (value => value > exclusiveMin), + message: nls.localize('validations.exclusiveMin', "Value must be strictly greater than {0}.", exclusiveMin) + }, + + { + enabled: prop.maximum !== undefined && (exclusiveMax === undefined || exclusiveMax > prop.maximum), + isValid: (value => value <= prop.maximum), + message: nls.localize('validations.max', "Value must be less than or equal to {0}.", prop.maximum) + }, + { + enabled: prop.minimum !== undefined && (exclusiveMin === undefined || exclusiveMin < prop.minimum), + isValid: (value => value >= prop.minimum), + message: nls.localize('validations.min', "Value must be greater than or equal to {0}.", prop.minimum) + }, + { + enabled: prop.multipleOf !== undefined, + isValid: (value => value % prop.multipleOf === 0), + message: nls.localize('validations.multipleOf', "Value must be a multiple of {0}.", prop.multipleOf) + }, + { + enabled: isIntegral, + isValid: (value => value % 1 === 0), + message: nls.localize('validations.expectedInteger', "Value must be an integer.") + }, + ].filter(validation => validation.enabled) : []; + + let stringValidations: Validator[] = [ + { + enabled: prop.maxLength !== undefined, + isValid: (value => value.length <= prop.maxLength), + message: nls.localize('validations.maxLength', "Value must be fewer than {0} characters long.", prop.maxLength) + }, + { + enabled: prop.minLength !== undefined, + isValid: (value => value.length >= prop.minLength), + message: nls.localize('validations.minLength', "Value must be more than {0} characters long.", prop.minLength) + }, + { + enabled: patternRegex !== undefined, + isValid: (value => patternRegex.test(value)), + message: prop.patternErrorMessage || nls.localize('validations.regex', "Value must match regex `{0}`.", prop.pattern) + }, + ].filter(validation => validation.enabled); + + if (prop.type === 'string' && stringValidations.length === 0) { return null; } + if (isNullable && value === '') { return ''; } + + let errors = []; + + if (isNumeric) { + if (value === '' || isNaN(+value)) { + errors.push(nls.localize('validations.expectedNumeric', "Value must be a number.")); + } else { + errors.push(...numericValidations.filter(validator => !validator.isValid(+value)).map(validator => validator.message)); + } + } + + if (prop.type === 'string') { + errors.push(...stringValidations.filter(validator => !validator.isValid('' + value)).map(validator => validator.message)); + } + if (errors.length) { + return prop.errorMessage ? [prop.errorMessage, ...errors].join(' ') : errors.join(' '); + } + return ''; + }; +} + +function escapeInvisibleChars(enumValue: string): string { + return enumValue && enumValue + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); +} + export function defaultKeybindingsContents(keybindingService: IKeybindingService): string { const defaultsHeader = '// ' + nls.localize('defaultKeybindingsHeader', "Overwrite key bindings by placing them into your key bindings file."); return defaultsHeader + '\n' + keybindingService.getDefaultKeybindingsContent(); diff --git a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts new file mode 100644 index 00000000000..8550a177cf4 --- /dev/null +++ b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.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 * as assert from 'assert'; +import { createValidator } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; + + +suite('Preferences Model test', () => { + class Tester { + private validator: (value: any) => string; + + constructor(private settings: IConfigurationPropertySchema) { + this.validator = createValidator(settings); + } + + public accepts(input) { + assert.equal(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to accept \`${input}\`. Got ${this.validator(input)}.`); + } + + public rejects(input) { + assert.notEqual(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to reject \`${input}\`.`); + return { + withMessage: + (message) => assert(this.validator(input).indexOf(message) > -1, + `Expected error of ${JSON.stringify(this.settings)} on \`${input}\` to contain ${message}. Got ${this.validator(input)}.`) + }; + } + + + public validatesNumeric() { + this.accepts('3'); + this.accepts('3.'); + this.accepts('.0'); + this.accepts('3.0'); + this.accepts(' 3.0'); + this.accepts(' 3.0 '); + this.rejects('3f'); + } + + public validatesNullableNumeric() { + this.validatesNumeric(); + this.accepts(''); + } + + public validatesNonNullableNumeric() { + this.validatesNumeric(); + this.rejects(''); + } + + public validatesString() { + this.accepts('3'); + this.accepts('3.'); + this.accepts('.0'); + this.accepts('3.0'); + this.accepts(' 3.0'); + this.accepts(' 3.0 '); + this.accepts(''); + this.accepts('3f'); + this.accepts('hello'); + } + } + + + test('exclusive max and max work together properly', () => { + { + const justMax = new Tester({ maximum: 5, type: 'number' }); + justMax.validatesNonNullableNumeric(); + justMax.rejects('5.1'); + justMax.accepts('5.0'); + } + { + const justEMax = new Tester({ exclusiveMaximum: 5, type: 'number' }); + justEMax.validatesNonNullableNumeric(); + justEMax.rejects('5.1'); + justEMax.rejects('5.0'); + justEMax.accepts('4.999'); + } + { + const bothNumeric = new Tester({ exclusiveMaximum: 5, maximum: 4, type: 'number' }); + bothNumeric.validatesNonNullableNumeric(); + bothNumeric.rejects('5.1'); + bothNumeric.rejects('5.0'); + bothNumeric.rejects('4.999'); + bothNumeric.accepts('4'); + } + { + const bothNumeric = new Tester({ exclusiveMaximum: 5, maximum: 6, type: 'number' }); + bothNumeric.validatesNonNullableNumeric(); + bothNumeric.rejects('5.1'); + bothNumeric.rejects('5.0'); + bothNumeric.accepts('4.999'); + } + }); + + test('exclusive min and min work together properly', () => { + { + const justMin = new Tester({ minimum: -5, type: 'number' }); + justMin.validatesNonNullableNumeric(); + justMin.rejects('-5.1'); + justMin.accepts('-5.0'); + } + { + const justEMin = new Tester({ exclusiveMinimum: -5, type: 'number' }); + justEMin.validatesNonNullableNumeric(); + justEMin.rejects('-5.1'); + justEMin.rejects('-5.0'); + justEMin.accepts('-4.999'); + } + { + const bothNumeric = new Tester({ exclusiveMinimum: -5, minimum: -4, type: 'number' }); + bothNumeric.validatesNonNullableNumeric(); + bothNumeric.rejects('-5.1'); + bothNumeric.rejects('-5.0'); + bothNumeric.rejects('-4.999'); + bothNumeric.accepts('-4'); + } + { + const bothNumeric = new Tester({ exclusiveMinimum: -5, minimum: -6, type: 'number' }); + bothNumeric.validatesNonNullableNumeric(); + bothNumeric.rejects('-5.1'); + bothNumeric.rejects('-5.0'); + bothNumeric.accepts('-4.999'); + } + }); + + test('multiple of works for both integers and fractions', () => { + { + const onlyEvens = new Tester({ multipleOf: 2, type: 'number' }); + onlyEvens.accepts('2.0'); + onlyEvens.accepts('2'); + onlyEvens.accepts('-4'); + onlyEvens.accepts('0'); + onlyEvens.accepts('100'); + onlyEvens.rejects('100.1'); + onlyEvens.rejects(''); + onlyEvens.rejects('we'); + } + { + const hackyIntegers = new Tester({ multipleOf: 1, type: 'number' }); + hackyIntegers.accepts('2.0'); + hackyIntegers.rejects('.5'); + } + { + const halfIntegers = new Tester({ multipleOf: 0.5, type: 'number' }); + halfIntegers.accepts('0.5'); + halfIntegers.accepts('1.5'); + halfIntegers.rejects('1.51'); + } + }); + + test('integer type correctly adds a validation', () => { + { + const integers = new Tester({ multipleOf: 1, type: 'integer' }); + integers.accepts('02'); + integers.accepts('2'); + integers.accepts('20'); + integers.rejects('.5'); + integers.rejects('2j'); + integers.rejects(''); + } + }); + + test('null is allowed only when expected', () => { + { + const nullableIntegers = new Tester({ type: ['integer', 'null'] }); + nullableIntegers.accepts('2'); + nullableIntegers.rejects('.5'); + nullableIntegers.accepts('2.0'); + nullableIntegers.rejects('2j'); + nullableIntegers.accepts(''); + } + { + const nonnullableIntegers = new Tester({ type: ['integer'] }); + nonnullableIntegers.accepts('2'); + nonnullableIntegers.rejects('.5'); + nonnullableIntegers.accepts('2.0'); + nonnullableIntegers.rejects('2j'); + nonnullableIntegers.rejects(''); + } + { + const nullableNumbers = new Tester({ type: ['number', 'null'] }); + nullableNumbers.accepts('2'); + nullableNumbers.accepts('.5'); + nullableNumbers.accepts('2.0'); + nullableNumbers.rejects('2j'); + nullableNumbers.accepts(''); + } + { + const nonnullableNumbers = new Tester({ type: ['number'] }); + nonnullableNumbers.accepts('2'); + nonnullableNumbers.accepts('.5'); + nonnullableNumbers.accepts('2.0'); + nonnullableNumbers.rejects('2j'); + nonnullableNumbers.rejects(''); + } + }); + + test('string max min length work', () => { + { + const min = new Tester({ minLength: 4, type: 'string' }); + min.rejects('123'); + min.accepts('1234'); + min.accepts('12345'); + } + { + const max = new Tester({ maxLength: 6, type: 'string' }); + max.accepts('12345'); + max.accepts('123456'); + max.rejects('1234567'); + } + { + const minMax = new Tester({ minLength: 4, maxLength: 6, type: 'string' }); + minMax.rejects('123'); + minMax.accepts('1234'); + minMax.accepts('12345'); + minMax.accepts('123456'); + minMax.rejects('1234567'); + } + }); + + test('patterns work', () => { + { + const urls = new Tester({ pattern: '^(hello)*$', type: 'string' }); + urls.accepts(''); + urls.rejects('hel'); + urls.accepts('hello'); + urls.rejects('hellohel'); + urls.accepts('hellohello'); + } + { + const urls = new Tester({ pattern: '^(hello)*$', type: 'string', patternErrorMessage: 'err: must be friendly' }); + urls.accepts(''); + urls.rejects('hel').withMessage('err: must be friendly'); + urls.accepts('hello'); + urls.rejects('hellohel').withMessage('err: must be friendly'); + urls.accepts('hellohello'); + } + }); + + test('custom error messages are shown', () => { + const withMessage = new Tester({ minLength: 1, maxLength: 0, type: 'string', errorMessage: 'always error!' }); + withMessage.rejects('').withMessage('always error!'); + withMessage.rejects(' ').withMessage('always error!'); + withMessage.rejects('1').withMessage('always error!'); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 6f6e6b5da76..b68cb4c8b77 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as lifecycle from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import * as types from 'vs/base/common/types'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -21,21 +21,20 @@ interface ProgressState { whileDelay?: number; } -export abstract class ScopedService { - - protected toDispose: lifecycle.IDisposable[]; +export abstract class ScopedService extends Disposable { constructor(private viewletService: IViewletService, private panelService: IPanelService, private scopeId: string) { - this.toDispose = []; + super(); + this.registerListeners(); } - public registerListeners(): void { - this.toDispose.push(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId()))); - this.toDispose.push(this.panelService.onDidPanelOpen(panel => this.onScopeOpened(panel.getId()))); + registerListeners(): void { + this._register(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId()))); + this._register(this.panelService.onDidPanelOpen(panel => this.onScopeOpened(panel.getId()))); - this.toDispose.push(this.viewletService.onDidViewletClose(viewlet => this.onScopeClosed(viewlet.getId()))); - this.toDispose.push(this.panelService.onDidPanelClose(panel => this.onScopeClosed(panel.getId()))); + this._register(this.viewletService.onDidViewletClose(viewlet => this.onScopeClosed(viewlet.getId()))); + this._register(this.panelService.onDidPanelClose(panel => this.onScopeClosed(panel.getId()))); } private onScopeClosed(scopeId: string) { @@ -50,13 +49,13 @@ export abstract class ScopedService { } } - public abstract onScopeActivated(): void; + abstract onScopeActivated(): void; - public abstract onScopeDeactivated(): void; + abstract onScopeDeactivated(): void; } export class ScopedProgressService extends ScopedService implements IProgressService { - public _serviceBrand: any; + _serviceBrand: any; private isActive: boolean; private progressbar: ProgressBar; private progressState: ProgressState; @@ -75,11 +74,11 @@ export class ScopedProgressService extends ScopedService implements IProgressSer this.progressState = Object.create(null); } - public onScopeDeactivated(): void { + onScopeDeactivated(): void { this.isActive = false; } - public onScopeActivated(): void { + onScopeActivated(): void { this.isActive = true; // Return early if progress state indicates that progress is done @@ -127,9 +126,9 @@ export class ScopedProgressService extends ScopedService implements IProgressSer this.progressState.whileDelay = void 0; } - public show(infinite: boolean, delay?: number): IProgressRunner; - public show(total: number, delay?: number): IProgressRunner; - public show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner { + show(infinite: boolean, delay?: number): IProgressRunner; + show(total: number, delay?: number): IProgressRunner; + show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner { let infinite: boolean; let total: number; @@ -207,7 +206,7 @@ export class ScopedProgressService extends ScopedService implements IProgressSer }; } - public showWhile(promise: TPromise, delay?: number): TPromise { + showWhile(promise: TPromise, delay?: number): TPromise { let stack: boolean = !!this.progressState.whilePromise; // Reset State @@ -252,21 +251,17 @@ export class ScopedProgressService extends ScopedService implements IProgressSer this.progressbar.infinite().show(delay); } } - - public dispose(): void { - this.toDispose = lifecycle.dispose(this.toDispose); - } } export class ProgressService implements IProgressService { - public _serviceBrand: any; + _serviceBrand: any; constructor(private progressbar: ProgressBar) { } - public show(infinite: boolean, delay?: number): IProgressRunner; - public show(total: number, delay?: number): IProgressRunner; - public show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner { + show(infinite: boolean, delay?: number): IProgressRunner; + show(total: number, delay?: number): IProgressRunner; + show(infiniteOrTotal: boolean | number, delay?: number): IProgressRunner { if (typeof infiniteOrTotal === 'boolean') { this.progressbar.infinite().show(delay); } else { @@ -292,7 +287,7 @@ export class ProgressService implements IProgressService { }; } - public showWhile(promise: TPromise, delay?: number): TPromise { + showWhile(promise: TPromise, delay?: number): TPromise { const stop = () => { this.progressbar.stop().hide(); }; diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 5a090430915..0720a1e14f2 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -8,17 +8,20 @@ import 'vs/css!./media/progressService2'; import * as dom from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IProgressService2, IProgressOptions, ProgressLocation, IProgress, IProgressStep, Progress, emptyProgress } from 'vs/platform/progress/common/progress'; +import { IProgressService2, IProgressOptions, IProgressStep, ProgressLocation } from 'vs/workbench/services/progress/common/progress'; +import { IProgress, emptyProgress, Progress } from 'vs/platform/progress/common/progress'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; -import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { TPromise } from 'vs/base/common/winjs.base'; -import { always } from 'vs/base/common/async'; +import { always, 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'; import { once } from 'vs/base/common/event'; +import { ViewContainer } from 'vs/workbench/common/views'; class WindowProgressItem implements IStatusbarItem { @@ -90,6 +93,15 @@ export class ProgressService2 implements IProgressService2 { withProgress

, R=any>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P { const { location } = options; + if (location instanceof ViewContainer) { + const viewlet = this._viewletService.getViewlet(location.id); + if (viewlet) { + return this._withViewletProgress(location.id, task); + } + console.warn(`Bad progress location: ${location.id}`); + return undefined; + } + switch (location) { case ProgressLocation.Notification: return this._withNotificationProgress(options, task, onDidCancel); @@ -119,8 +131,8 @@ export class ProgressService2 implements IProgressService2 { this._updateWindowProgress(); // show progress for at least 150ms - always(TPromise.join([ - TPromise.timeout(150), + always(Promise.all([ + timeout(150), promise ]), () => { const idx = this._stack.indexOf(task); @@ -131,7 +143,7 @@ export class ProgressService2 implements IProgressService2 { }, 150); // cancel delay if promise finishes below 150ms - always(TPromise.wrap(promise), () => clearTimeout(delayHandle)); + always(promise, () => clearTimeout(delayHandle)); return promise; } @@ -259,7 +271,7 @@ export class ProgressService2 implements IProgressService2 { }); // Show progress for at least 800ms and then hide once done or canceled - always(TPromise.join([TPromise.timeout(800), p]), () => { + always(Promise.all([timeout(800), p]), () => { if (handle) { handle.close(); } diff --git a/src/vs/workbench/services/progress/common/progress.ts b/src/vs/workbench/services/progress/common/progress.ts new file mode 100644 index 00000000000..bf9d2a53725 --- /dev/null +++ b/src/vs/workbench/services/progress/common/progress.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ViewContainer } from 'vs/workbench/common/views'; + +export const enum ProgressLocation { + Explorer = 1, + Scm = 3, + Extensions = 5, + Window = 10, + Notification = 15 +} + +export interface IProgressOptions { + location: ProgressLocation | ViewContainer; + title?: string; + source?: string; + total?: number; + cancellable?: boolean; +} + +export interface IProgressStep { + message?: string; + increment?: number; +} + +export const IProgressService2 = createDecorator('progressService2'); + +export interface IProgressService2 { + + _serviceBrand: any; + + withProgress

, R=any>(options: IProgressOptions, task: (progress: IProgress) => P, onDidCancel?: () => void): P; +} \ No newline at end of file diff --git a/src/vs/workbench/services/progress/test/progressService.test.ts b/src/vs/workbench/services/progress/test/progressService.test.ts index f1f23e0cea1..67dd539396e 100644 --- a/src/vs/workbench/services/progress/test/progressService.test.ts +++ b/src/vs/workbench/services/progress/test/progressService.test.ts @@ -40,6 +40,10 @@ class TestViewletService implements IViewletService { return []; } + public getAllViewlets(): ViewletDescriptor[] { + return []; + } + public getActiveViewlet(): IViewlet { return activeViewlet; } diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index 6a01974b0ee..f5597892346 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -6,7 +6,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -36,7 +36,7 @@ export interface ISCMResource { readonly resourceGroup: ISCMResourceGroup; readonly sourceUri: URI; readonly decorations: ISCMResourceDecorations; - open(): TPromise; + open(): Thenable; } export interface ISCMResourceGroup extends ISequence { @@ -68,7 +68,7 @@ export interface ISCMProvider extends IDisposable { getOriginalResource(uri: URI): TPromise; } -export enum InputValidationType { +export const enum InputValidationType { Error = 0, Warning = 1, Information = 2 @@ -96,9 +96,12 @@ export interface ISCMInput { export interface ISCMRepository extends IDisposable { readonly onDidFocus: Event; + readonly selected: boolean; + readonly onDidChangeSelection: Event; readonly provider: ISCMProvider; readonly input: ISCMInput; focus(): void; + setSelected(selected: boolean): void; } export interface ISCMService { @@ -108,6 +111,8 @@ export interface ISCMService { readonly onDidRemoveRepository: Event; readonly repositories: ISCMRepository[]; + readonly selectedRepositories: ISCMRepository[]; + readonly onDidChangeSelectedRepositories: Event; registerSCMProvider(provider: ISCMProvider): ISCMRepository; } diff --git a/src/vs/workbench/services/scm/common/scmService.ts b/src/vs/workbench/services/scm/common/scmService.ts index d062ae05633..83b3e88e06d 100644 --- a/src/vs/workbench/services/scm/common/scmService.ts +++ b/src/vs/workbench/services/scm/common/scmService.ts @@ -10,6 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; import { TPromise } from 'vs/base/common/winjs.base'; +import { equals } from 'vs/base/common/arrays'; class SCMInput implements ISCMInput { @@ -61,6 +62,14 @@ class SCMRepository implements ISCMRepository { private _onDidFocus = new Emitter(); readonly onDidFocus: Event = this._onDidFocus.event; + private _selected = false; + get selected(): boolean { + return this._selected; + } + + private _onDidChangeSelection = new Emitter(); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + readonly input: ISCMInput = new SCMInput(); constructor( @@ -72,6 +81,11 @@ class SCMRepository implements ISCMRepository { this._onDidFocus.fire(); } + setSelected(selected: boolean): void { + this._selected = selected; + this._onDidChangeSelection.fire(selected); + } + dispose(): void { this.disposable.dispose(); this.provider.dispose(); @@ -86,6 +100,12 @@ export class SCMService implements ISCMService { private _repositories: ISCMRepository[] = []; get repositories(): ISCMRepository[] { return [...this._repositories]; } + private _selectedRepositories: ISCMRepository[] = []; + get selectedRepositories(): ISCMRepository[] { return [...this._selectedRepositories]; } + + private _onDidChangeSelectedRepositories = new Emitter(); + readonly onDidChangeSelectedRepositories: Event = this._onDidChangeSelectedRepositories.event; + private _onDidAddProvider = new Emitter(); get onDidAddRepository(): Event { return this._onDidAddProvider.event; } @@ -110,15 +130,35 @@ export class SCMService implements ISCMService { return; } + selectedDisposable.dispose(); this._providerIds.delete(provider.id); this._repositories.splice(index, 1); this._onDidRemoveProvider.fire(repository); + this.onDidChangeSelection(); }); const repository = new SCMRepository(provider, disposable); + const selectedDisposable = repository.onDidChangeSelection(this.onDidChangeSelection, this); + this._repositories.push(repository); this._onDidAddProvider.fire(repository); + // automatically select the first repository + if (this._repositories.length === 1) { + repository.setSelected(true); + } + return repository; } + + private onDidChangeSelection(): void { + const selectedRepositories = this._repositories.filter(r => r.selected); + + if (equals(this._selectedRepositories, selectedRepositories)) { + return; + } + + this._selectedRepositories = this._repositories.filter(r => r.selected); + this._onDidChangeSelectedRepositories.fire(this.selectedRepositories); + } } diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 6e5b4a853ce..d86cbb9b37f 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -6,28 +6,27 @@ 'use strict'; import * as childProcess from 'child_process'; -import { StringDecoder, NodeStringDecoder } from 'string_decoder'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as fs from 'fs'; import * as path from 'path'; -import { isEqualOrParent } from 'vs/base/common/paths'; import { Readable } from 'stream'; -import { TPromise } from 'vs/base/common/winjs.base'; - -import * as objects from 'vs/base/common/objects'; +import { NodeStringDecoder, StringDecoder } from 'string_decoder'; import * as arrays from 'vs/base/common/arrays'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import * as glob from 'vs/base/common/glob'; +import * as normalization from 'vs/base/common/normalization'; +import * as objects from 'vs/base/common/objects'; +import { isEqualOrParent } from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import * as normalization from 'vs/base/common/normalization'; import * as types from 'vs/base/common/types'; -import * as glob from 'vs/base/common/glob'; -import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search'; - +import { TPromise } from 'vs/base/common/winjs.base'; import * as extfs from 'vs/base/node/extfs'; import * as flow from 'vs/base/node/flow'; -import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search'; +import { IProgress, ISearchEngineStats } from 'vs/platform/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { rgErrorMsgForDisplay } from './ripgrepTextSearch'; +import { IFolderSearch, IRawFileMatch, IRawSearch, ISearchEngine, ISearchEngineSuccess } from './search'; +import { StopWatch } from 'vs/base/common/stopwatch'; enum Traversal { Node = 1, @@ -60,13 +59,12 @@ export class FileWalker { private isLimitHit: boolean; private resultCount: number; private isCanceled: boolean; - private fileWalkStartTime: number; + private fileWalkSW: StopWatch; private directoriesWalked: number; private filesWalked: number; private traversal: Traversal; private errors: string[]; - private cmdForkStartTime: number; - private cmdForkResultTime: number; + private cmdSW: StopWatch; private cmdResultCount: number; private folderExcludePatterns: Map; @@ -120,81 +118,65 @@ export class FileWalker { } public walk(folderQueries: IFolderSearch[], extraFiles: string[], onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, isLimitHit: boolean) => void): void { - this.fileWalkStartTime = Date.now(); + this.fileWalkSW = StopWatch.create(false); // Support that the file pattern is a full path to a file that exists - this.checkFilePatternAbsoluteMatch((exists, size) => { - if (this.isCanceled) { - return done(null, this.isLimitHit); - } + if (this.isCanceled) { + return done(null, this.isLimitHit); + } - // Report result from file pattern if matching - if (exists) { - this.resultCount++; - onResult({ - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - size - }); - - // Optimization: a match on an absolute path is a good result and we do not - // continue walking the entire root paths array for other matches because - // it is very unlikely that another file would match on the full absolute path - return done(null, this.isLimitHit); - } - - // For each extra file - if (extraFiles) { - extraFiles.forEach(extraFilePath => { - const basename = path.basename(extraFilePath); - if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) { - return; // excluded - } - - // File: Check for match on file pattern and include pattern - this.matchFile(onResult, { relativePath: extraFilePath /* 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; - // Disable 'dir' for now (#11181, #11179, #11183, #11182). - } /* else if (platform.isWindows) { - this.traversal = Traversal.WindowsDir; - traverse = this.windowsDirTraversal; - } */ else if (platform.isLinux) { - this.traversal = Traversal.LinuxFind; - traverse = this.cmdTraversal; + // For each extra file + if (extraFiles) { + extraFiles.forEach(extraFilePath => { + const basename = path.basename(extraFilePath); + if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) { + return; // excluded } - } - const isNodeTraversal = traverse === this.nodeJSTraversal; - if (!isNodeTraversal) { - this.cmdForkStartTime = Date.now(); - } - - // For each root folder - flow.parallel(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => { - this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => { - if (err) { - const errorMessage = toErrorMessage(err); - console.error(errorMessage); - this.errors.push(errorMessage); - rootFolderDone(err, undefined); - } else { - rootFolderDone(undefined, undefined); - } - }); - }, (errors, result) => { - const err = errors ? errors.filter(e => !!e)[0] : null; - done(err, this.isLimitHit); + // File: Check for match on file pattern and include pattern + this.matchFile(onResult, { relativePath: extraFilePath /* 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; + // Disable 'dir' for now (#11181, #11179, #11183, #11182). + } /* else if (platform.isWindows) { + this.traversal = Traversal.WindowsDir; + traverse = this.windowsDirTraversal; + } */ else if (platform.isLinux) { + this.traversal = Traversal.LinuxFind; + traverse = this.cmdTraversal; + } + } + + const isNodeTraversal = traverse === this.nodeJSTraversal; + if (!isNodeTraversal) { + this.cmdSW = StopWatch.create(false); + } + + // For each root folder + flow.parallel(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => { + this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => { + if (err) { + const errorMessage = toErrorMessage(err); + console.error(errorMessage); + this.errors.push(errorMessage); + rootFolderDone(err, undefined); + } else { + rootFolderDone(undefined, undefined); + } + }); + }, (errors, result) => { + this.fileWalkSW.stop(); + const err = errors ? errors.filter(e => !!e)[0] : null; + done(err, this.isLimitHit); }); } @@ -223,29 +205,27 @@ export class FileWalker { const useRipgrep = this.useRipgrep; let noSiblingsClauses: boolean; - let filePatternSeen = false; if (useRipgrep) { const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression); cmd = ripgrep.cmd; noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; - process.nextTick(() => { - 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 }); - }); + 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); } process.on('exit', killCmd); - this.collectStdout(cmd, 'utf8', useRipgrep, (err: Error, stdout?: string, last?: boolean) => { + this.cmdResultCount = 0; + this.collectStdout(cmd, 'utf8', useRipgrep, onMessage, (err: Error, stdout?: string, last?: boolean) => { if (err) { done(err); return; @@ -282,9 +262,6 @@ export class FileWalker { if (useRipgrep && noSiblingsClauses) { for (const relativePath of relativeFiles) { - if (relativePath === this.filePattern) { - filePatternSeen = true; - } const basename = path.basename(relativePath); this.matchFile(onResult, { base: rootFolder, relativePath, basename }); if (this.isLimitHit) { @@ -293,22 +270,9 @@ export class FileWalker { } } if (last || this.isLimitHit) { - if (!filePatternSeen) { - this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - }); - } - done(); - }); - } else { - done(); - } + done(); } + return; } @@ -379,7 +343,7 @@ export class FileWalker { */ public readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string) => void): void { let all = ''; - this.collectStdout(cmd, encoding, isRipgrep, (err: Error, stdout?: string, last?: boolean) => { + this.collectStdout(cmd, encoding, isRipgrep, () => { }, (err: Error, stdout?: string, last?: boolean) => { if (err) { cb(err); return; @@ -392,35 +356,49 @@ export class FileWalker { }); } - private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string, last?: boolean) => void): void { - let done = (err: Error, stdout?: string, last?: boolean) => { + private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, onMessage: (message: IProgress) => void, cb: (err: Error, stdout?: string, last?: boolean) => void): void { + let onData = (err: Error, stdout?: string, last?: boolean) => { if (err || last) { - done = () => { }; - this.cmdForkResultTime = Date.now(); + onData = () => { }; + + if (this.cmdSW) { + this.cmdSW.stop(); + } } cb(err, stdout, last); }; - this.forwardData(cmd.stdout, encoding, done); - const stderr = this.collectData(cmd.stderr); - let gotData = false; - cmd.stdout.once('data', () => gotData = true); + if (cmd.stdout) { + // Should be non-null, but #38195 + this.forwardData(cmd.stdout, encoding, onData); + cmd.stdout.once('data', () => gotData = true); + } else { + onMessage({ message: 'stdout is null' }); + } + + let stderr: Buffer[]; + if (cmd.stderr) { + // Should be non-null, but #38195 + stderr = this.collectData(cmd.stderr); + } else { + onMessage({ message: 'stderr is null' }); + } cmd.on('error', (err: Error) => { - done(err); + onData(err); }); cmd.on('close', (code: number) => { // ripgrep returns code=1 when no results are found let stderrText, displayMsg: string; if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && (displayMsg = rgErrorMsgForDisplay(stderrText))) : code !== 0) { - done(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`)); + onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`)); } else { if (isRipgrep && this.exists && code === 0) { this.isLimitHit = true; } - done(null, '', true); + onData(null, '', true); } }); } @@ -485,6 +463,7 @@ export class FileWalker { const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { self.directoriesWalked++; + const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -493,7 +472,7 @@ export class FileWalker { // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) { + if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -524,70 +503,30 @@ export class FileWalker { return done(); } - // Support relative paths to files from a root resource (ignores excludes) - return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => { - if (this.isCanceled || this.isLimitHit) { - return done(); - } + if (this.isCanceled || this.isLimitHit) { + return done(); + } - // Report result from file pattern if matching - if (match) { - this.resultCount++; - onResult({ - base: folderQuery.folder, - relativePath: this.filePattern, - basename: path.basename(this.filePattern), - size - }); - } - - return this.doWalk(folderQuery, '', files, onResult, done); - }); + return this.doWalk(folderQuery, '', files, onResult, done); }); } - public getStats(): IUncachedSearchStats { + public getStats(): ISearchEngineStats { return { - fromCache: false, + cmdTime: this.cmdSW && this.cmdSW.elapsed(), + fileWalkTime: this.fileWalkSW.elapsed(), traversal: Traversal[this.traversal], - errors: this.errors, - fileWalkStartTime: this.fileWalkStartTime, - fileWalkResultTime: Date.now(), directoriesWalked: this.directoriesWalked, filesWalked: this.filesWalked, - resultCount: this.resultCount, - cmdForkStartTime: this.cmdForkStartTime, - cmdForkResultTime: this.cmdForkResultTime, cmdResultCount: this.cmdResultCount }; } - private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void { - if (!this.filePattern || !path.isAbsolute(this.filePattern)) { - return clb(false); - } - - return fs.stat(this.filePattern, (error, stat) => { - return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files - }); - } - - private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void { - if (!this.filePattern || path.isAbsolute(this.filePattern)) { - return clb(null); - } - - const absolutePath = path.join(basePath, this.filePattern); - - return fs.stat(absolutePath, (error, stat) => { - return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files - }); - } - private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void { const rootFolder = folderQuery.folder; // Execute tasks on each file in parallel to optimize throughput + const hasSibling = glob.hasSiblingFn(() => files); flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => { // Check canceled @@ -595,17 +534,12 @@ export class FileWalker { return clb(null, undefined); } + // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching // to ignore filtering by siblings because the user seems to know what she // is searching for and we want to include the result in that case anyway - let siblings = files; - if (this.config.filePattern === file) { - siblings = []; - } - - // Check exclude pattern let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file; - if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) { + if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) { return clb(null, undefined); } @@ -742,7 +676,7 @@ export class Engine implements ISearchEngine { this.walker = new FileWalker(config); } - public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISearchEngineSuccess) => void): void { this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => { done(err, { limitHit: isLimitHit, @@ -791,9 +725,9 @@ class AbsoluteAndRelativeParsedExpression { this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true }); } - public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise): string | TPromise { - return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) || - (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn)); + public test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise): string | TPromise { + return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) || + (this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling)); } public getBasenameTerms(): string[] { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 7f8387e9d29..0e3f6d5c068 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -6,23 +6,30 @@ 'use strict'; import * as fs from 'fs'; -import { isAbsolute, sep, join } from 'path'; - import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); - +import { join, sep } from 'path'; import * as arrays from 'vs/base/common/arrays'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; +import { StopWatch } from 'vs/base/common/stopwatch'; import * as strings from 'vs/base/common/strings'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; +import { ICachedSearchStats, IFileSearchStats, IProgress } from 'vs/platform/search/common/search'; +import { Engine as FileSearchEngine, FileWalker } from 'vs/workbench/services/search/node/fileSearch'; import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch'; import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch'; import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider'; -import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem, ITelemetryEvent } from './search'; -import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search'; -import { compareItemsByScore, IItemAccessor, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from './search'; + +gracefulFs.gracefulify(fs); + +type IProgressCallback = (p: ISerializedSearchProgressItem) => void; +type IFileProgressCallback = (p: IFileSearchProgressItem) => void; export class SearchService implements IRawSearchService { @@ -32,29 +39,61 @@ export class SearchService implements IRawSearchService { private textSearchWorkerProvider: TextSearchWorkerProvider; - private telemetryPipe: (event: ITelemetryEvent) => void; + public fileSearch(config: IRawSearch, batchSize = SearchService.BATCH_SIZE): Event { + let promise: CancelablePromise; - public fileSearch(config: IRawSearch): PPromise { - return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE); + const emitter = new Emitter({ + onFirstListenerDidAdd: () => { + promise = createCancelablePromise(token => { + return this.doFileSearch(FileSearchEngine, config, p => emitter.fire(p), token, batchSize); + }); + + promise.then( + c => emitter.fire(c), + err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } })); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; } - public textSearch(config: IRawSearch): PPromise { - return config.useRipgrep ? - this.ripgrepTextSearch(config) : - this.legacyTextSearch(config); + public textSearch(config: IRawSearch): Event { + let promise: CancelablePromise; + + const emitter = new Emitter({ + onFirstListenerDidAdd: () => { + promise = createCancelablePromise(token => { + return (config.useRipgrep ? this.ripgrepTextSearch(config, p => emitter.fire(p), token) : this.legacyTextSearch(config, p => emitter.fire(p), token)); + }); + + promise.then( + c => emitter.fire(c), + err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } })); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; } - public ripgrepTextSearch(config: IRawSearch): PPromise { + private ripgrepTextSearch(config: IRawSearch, progressCallback: IProgressCallback, token: CancellationToken): Promise { config.maxFilesize = MAX_FILE_SIZE; let engine = new RipgrepEngine(config); - return new PPromise((c, e, p) => { + 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(SearchService.BATCH_SIZE, p); + const collector = new BatchedCollector(SearchService.BATCH_SIZE, progressCallback); engine.search((match) => { collector.addItem(match, match.numMatches); }, (message) => { - p(message); + progressCallback(message); }, (error, stats) => { collector.flush(); @@ -64,12 +103,10 @@ export class SearchService implements IRawSearchService { c(stats); } }); - }, () => { - engine.cancel(); }); } - public legacyTextSearch(config: IRawSearch): PPromise { + private legacyTextSearch(config: IRawSearch, progressCallback: IProgressCallback, token: CancellationToken): Promise { if (!this.textSearchWorkerProvider) { this.textSearchWorkerProvider = new TextSearchWorkerProvider(); } @@ -87,47 +124,54 @@ export class SearchService implements IRawSearchService { }), this.textSearchWorkerProvider); - return this.doTextSearch(engine, SearchService.BATCH_SIZE); + return this.doTextSearch(engine, progressCallback, SearchService.BATCH_SIZE, token); } - public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, batchSize?: number): PPromise { + doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, progressCallback: IProgressCallback, token?: CancellationToken, batchSize?: number): TPromise { + let resultCount = 0; + const fileProgressCallback: IFileProgressCallback = progress => { + if (Array.isArray(progress)) { + resultCount += progress.length; + progressCallback(progress.map(m => this.rawMatchToSearchItem(m))); + } else if ((progress).relativePath) { + resultCount++; + progressCallback(this.rawMatchToSearchItem(progress)); + } else { + progressCallback(progress); + } + }; if (config.sortByScore) { - let sortedSearch = this.trySortedSearchFromCache(config); + let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback, token); if (!sortedSearch) { const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config; const engine = new EngineClass(walkerConfig); - sortedSearch = this.doSortedSearch(engine, config); + sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback, token); } - return new PPromise((c, e, p) => { - process.nextTick(() => { // allow caller to register progress callback first - sortedSearch.then(([result, rawMatches]) => { - const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); - this.sendProgress(serializedMatches, p, batchSize); - c(result); - }, e, p); - }); - }, () => { - sortedSearch.cancel(); + return new TPromise((c, e) => { + sortedSearch.then(([result, rawMatches]) => { + const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch)); + this.sendProgress(serializedMatches, progressCallback, batchSize); + c(result); + }, e); }); } - let searchPromise: PPromise; - return new PPromise((c, e, p) => { - const engine = new EngineClass(config); - searchPromise = this.doSearch(engine, batchSize) - .then(c, e, progress => { - if (Array.isArray(progress)) { - p(progress.map(m => this.rawMatchToSearchItem(m))); - } else if ((progress).relativePath) { - p(this.rawMatchToSearchItem(progress)); - } else { - p(progress); - } - }); - }, () => { - searchPromise.cancel(); + const engine = new EngineClass(config); + + return this.doSearch(engine, fileProgressCallback, batchSize, token).then(complete => { + return { + limitHit: complete.limitHit, + type: 'success', + stats: { + detailStats: complete.stats, + type: 'searchProcess', + fromCache: false, + resultCount, + sortingTime: undefined + } + }; }); } @@ -135,62 +179,70 @@ export class SearchService implements IRawSearchService { return { path: match.base ? join(match.base, match.relativePath) : match.relativePath }; } - private doSortedSearch(engine: ISearchEngine, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> { - let searchPromise: PPromise; - let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => { + private doSortedSearch(engine: ISearchEngine, config: IRawSearch, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback, token?: CancellationToken): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> { + const emitter = new Emitter(); + + let allResultsPromise = createCancelablePromise(token => { let results: IRawFileMatch[] = []; - searchPromise = this.doSearch(engine, -1) - .then(result => { - c([result, results]); - if (this.telemetryPipe) { - // __GDPR__TODO__ classify event - this.telemetryPipe({ - eventName: 'fileSearch', - data: result.stats - }); - } - }, e, progress => { - if (Array.isArray(progress)) { - results = progress; - } else { - p(progress); - } + + const innerProgressCallback: IFileProgressCallback = progress => { + if (Array.isArray(progress)) { + results = progress; + } else { + fileProgressCallback(progress); + emitter.fire(progress); + } + }; + + return this.doSearch(engine, innerProgressCallback, -1, token) + .then<[ISearchEngineSuccess, IRawFileMatch[]]>(result => { + return [result, results]; }); - }, () => { - searchPromise.cancel(); }); let cache: Cache; if (config.cacheKey) { cache = this.getOrCreateCache(config.cacheKey); - cache.resultsToSearchCache[config.filePattern] = allResultsPromise; - allResultsPromise.then(null, err => { + const cacheRow: ICacheRow = { + promise: allResultsPromise, + event: emitter.event, + resolved: false + }; + cache.resultsToSearchCache[config.filePattern] = cacheRow; + allResultsPromise.then(() => { + cacheRow.resolved = true; + }, err => { delete cache.resultsToSearchCache[config.filePattern]; }); + allResultsPromise = this.preventCancellation(allResultsPromise); } - let chained: TPromise; - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => { - chained = allResultsPromise.then(([result, results]) => { + return TPromise.wrap<[ISerializedSearchSuccess, IRawFileMatch[]]>( + allResultsPromise.then(([result, results]) => { const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null); - const unsortedResultTime = Date.now(); - return this.sortResults(config, results, scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); + const sortSW = (typeof config.maxResults !== 'number' || config.maxResults > 0) && StopWatch.create(false); + return this.sortResults(config, results, scorerCache, token) + .then<[ISerializedSearchSuccess, IRawFileMatch[]]>(sortedResults => { + // sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickopen is opened. + // Contrasting with findFiles which is not sorted and will have sortingTime: undefined + const sortingTime = sortSW ? sortSW.elapsed() : -1; - c([{ - stats: objects.assign({}, result.stats, { - unsortedResultTime, - sortedResultTime - }), + return [{ + type: 'success', + stats: { + detailStats: result.stats, + sortingTime, + fromCache: false, + type: 'searchProcess', + workspaceFolderCount: config.folderQueries.length, + resultCount: sortedResults.length + }, limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults - }, sortedResults]); + } as ISerializedSearchSuccess, sortedResults]; }); - }, e, p); - }, () => { - chained.cancel(); - }); + }) + ); } private getOrCreateCache(cacheKey: string): Cache { @@ -201,55 +253,42 @@ export class SearchService implements IRawSearchService { return this.caches[cacheKey] = new Cache(); } - private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> { + private trySortedSearchFromCache(config: IRawSearch, progressCallback: IFileProgressCallback, token?: CancellationToken): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> { const cache = config.cacheKey && this.caches[config.cacheKey]; if (!cache) { return undefined; } - const cacheLookupStartTime = Date.now(); - const cached = this.getResultsFromCache(cache, config.filePattern); + const cached = this.getResultsFromCache(cache, config.filePattern, progressCallback, token); if (cached) { - let chained: TPromise; - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => { - chained = cached.then(([result, results, cacheStats]) => { - const cacheLookupResultTime = Date.now(); - return this.sortResults(config, results, cache.scorerCache) - .then(sortedResults => { - const sortedResultTime = Date.now(); + return cached.then(([result, results, cacheStats]) => { + const sortSW = StopWatch.create(false); + return this.sortResults(config, results, cache.scorerCache, token) + .then<[ISerializedSearchSuccess, IRawFileMatch[]]>(sortedResults => { + const sortingTime = sortSW.elapsed(); + const stats: IFileSearchStats = { + fromCache: true, + detailStats: cacheStats, + type: 'searchProcess', + resultCount: results.length, + sortingTime + }; - const stats: ICachedSearchStats = { - fromCache: true, - cacheLookupStartTime: cacheLookupStartTime, - cacheFilterStartTime: cacheStats.cacheFilterStartTime, - cacheLookupResultTime: cacheLookupResultTime, - cacheEntryCount: cacheStats.cacheFilterResultCount, - resultCount: results.length - }; - if (config.sortByScore) { - stats.unsortedResultTime = cacheLookupResultTime; - stats.sortedResultTime = sortedResultTime; - } - if (!cacheStats.cacheWasResolved) { - stats.joined = result.stats; - } - c([ - { - limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults, - stats: stats - }, - sortedResults - ]); - }); - }, e, p); - }, () => { - chained.cancel(); + return [ + { + type: 'success', + limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults, + stats + } as ISerializedSearchSuccess, + sortedResults + ]; + }); }); } return undefined; } - private sortResults(config: IRawSearch, results: IRawFileMatch[], scorerCache: ScorerCache): TPromise { + private sortResults(config: IRawSearch, results: IRawFileMatch[], scorerCache: ScorerCache, token?: CancellationToken): TPromise { // we use the same compare function that is used later when showing the results using fuzzy scoring // this is very important because we are also limiting the number of results by config.maxResults // and as such we want the top items to be included in this result set if the number of items @@ -257,10 +296,10 @@ export class SearchService implements IRawSearchService { const query = prepareQuery(config.filePattern); const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); - return arrays.topAsync(results, compare, config.maxResults, 10000); + return arrays.topAsync(results, compare, config.maxResults, 10000, token); } - private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) { + private sendProgress(results: ISerializedFileMatch[], progressCb: IProgressCallback, batchSize: number) { if (batchSize && batchSize > 0) { for (let i = 0; i < results.length; i += batchSize) { progressCb(results.slice(i, i + batchSize)); @@ -270,118 +309,130 @@ export class SearchService implements IRawSearchService { } } - private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> { - if (isAbsolute(searchValue)) { - return null; // bypass cache if user looks up an absolute path where matching goes directly on disk - } + private getResultsFromCache(cache: Cache, searchValue: string, progressCallback: IFileProgressCallback, token?: CancellationToken): TPromise<[ISearchEngineSuccess, IRawFileMatch[], ICachedSearchStats]> { + const cacheLookupSW = StopWatch.create(false); // Find cache entries by prefix of search value const hasPathSep = searchValue.indexOf(sep) >= 0; - let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; - let wasResolved: boolean; + let cachedRow: ICacheRow; for (let previousSearch in cache.resultsToSearchCache) { - // If we narrow down, we might be able to reuse the cached results if (strings.startsWith(searchValue, previousSearch)) { if (hasPathSep && previousSearch.indexOf(sep) < 0) { continue; // since a path character widens the search for potential more matches, require it in previous search too } - const c = cache.resultsToSearchCache[previousSearch]; - c.then(() => { wasResolved = false; }); - wasResolved = true; - cached = this.preventCancellation(c); + const row = cache.resultsToSearchCache[previousSearch]; + cachedRow = { + promise: this.preventCancellation(row.promise), + event: row.event, + resolved: row.resolved + }; break; } } - if (!cached) { + if (!cachedRow) { return null; } - return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => { - cached.then(([complete, cachedEntries]) => { - const cacheFilterStartTime = Date.now(); + const cacheLookupTime = cacheLookupSW.elapsed(); + const cacheFilterSW = StopWatch.create(false); - // Pattern match on results - let results: IRawFileMatch[] = []; - const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); - for (let i = 0; i < cachedEntries.length; i++) { - let entry = cachedEntries[i]; + const listener = cachedRow.event(progressCallback); + if (token) { + token.onCancellationRequested(() => { + listener.dispose(); + }); + } - // Check if this entry is a match for the search value - if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { - continue; - } + return TPromise.wrap(cachedRow.promise.then<[ISearchEngineSuccess, IRawFileMatch[], ICachedSearchStats]>(([complete, cachedEntries]) => { + if (token && token.isCancellationRequested) { + throw canceled(); + } - results.push(entry); + // Pattern match on results + let results: IRawFileMatch[] = []; + const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); + for (let i = 0; i < cachedEntries.length; i++) { + let entry = cachedEntries[i]; + + // Check if this entry is a match for the search value + if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { + continue; } - c([complete, results, { - cacheWasResolved: wasResolved, - cacheFilterStartTime: cacheFilterStartTime, - cacheFilterResultCount: cachedEntries.length - }]); - }, e, p); - }, () => { - cached.cancel(); - }); + results.push(entry); + } + + return [complete, results, { + cacheWasResolved: cachedRow.resolved, + cacheLookupTime, + cacheFilterTime: cacheFilterSW.elapsed(), + cacheEntryCount: cachedEntries.length + }]; + })); } - private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise { - return new PPromise((c, e, p) => { + private doTextSearch(engine: TextSearchEngine, progressCallback: IProgressCallback, batchSize: number, token: CancellationToken): Promise { + 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, p); + const collector = new BatchedCollector(batchSize, progressCallback); engine.search((matches) => { const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0); collector.addItems(matches, totalMatches); }, (progress) => { - p(progress); + progressCallback(progress); }, (error, stats) => { collector.flush(); if (error) { e(error); } else { - c(stats); + c({ + type: 'success', + limitHit: stats.limitHit, + stats: null + }); } }); - }, () => { - engine.cancel(); }); } - private doSearch(engine: ISearchEngine, batchSize?: number): PPromise { - return new PPromise((c, e, p) => { + private doSearch(engine: ISearchEngine, progressCallback: IFileProgressCallback, batchSize: number, token?: CancellationToken): TPromise { + return new TPromise((c, e) => { let batch: IRawFileMatch[] = []; + if (token) { + token.onCancellationRequested(() => engine.cancel()); + } + engine.search((match) => { if (match) { if (batchSize) { batch.push(match); if (batchSize > 0 && batch.length >= batchSize) { - p(batch); + progressCallback(batch); batch = []; } } else { - p(match); + progressCallback(match); } } }, (progress) => { - process.nextTick(() => { - p(progress); - }); - }, (error, stats) => { + progressCallback(progress); + }, (error, complete) => { if (batch.length) { - p(batch); + progressCallback(batch); } + if (error) { e(error); } else { - c(stats); + c(complete); } }); - }, () => { - engine.cancel(); }); } @@ -390,29 +441,35 @@ export class SearchService implements IRawSearchService { return TPromise.as(undefined); } - public fetchTelemetry(): PPromise { - return new PPromise((c, e, p) => { - this.telemetryPipe = p; - }, () => { - this.telemetryPipe = null; - }); + /** + * Return a CancelablePromise which is not actually cancelable + * TODO@rob - Is this really needed? + */ + private preventCancellation(promise: CancelablePromise): CancelablePromise { + return new class implements CancelablePromise { + cancel() { + // Do nothing + } + then(resolve, reject) { + return promise.then(resolve, reject); + } + catch(reject?) { + return this.then(undefined, reject); + } + }; } +} - private preventCancellation(promise: PPromise): PPromise { - return new PPromise((c, e, p) => { - // Allow for piled up cancellations to come through first. - process.nextTick(() => { - promise.then(c, e, p); - }); - }, () => { - // Do not propagate. - }); - } +interface ICacheRow { + // TODO@roblou - never actually canceled + promise: CancelablePromise<[ISearchEngineSuccess, IRawFileMatch[]]>; + resolved: boolean; + event: Event; } class Cache { - public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null); + public resultsToSearchCache: { [searchValue: string]: ICacheRow; } = Object.create(null); public scorerCache: ScorerCache = Object.create(null); } @@ -432,12 +489,6 @@ const FileMatchItemAccessor = new class implements IItemAccessor } }; -interface CacheStats { - cacheWasResolved: boolean; - cacheFilterStartTime: number; - cacheFilterResultCount: number; -} - /** * Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every * set of items collected. diff --git a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts index e3fa4591f8c..0d2c576b0d9 100644 --- a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts @@ -74,6 +74,9 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter args.push('--quiet'); } + args.push('--no-config'); + args.push('--no-ignore-global'); + // Folder to search args.push('--'); diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index 05ecdc78c93..94751dc40b8 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -4,24 +4,22 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as cp from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'path'; -import { StringDecoder, NodeStringDecoder } from 'string_decoder'; - -import * as cp from 'child_process'; -import { rgPath } from 'vscode-ripgrep'; - +import { NodeStringDecoder, StringDecoder } from 'string_decoder'; +import * as glob from 'vs/base/common/glob'; import * as objects from 'vs/base/common/objects'; +import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import * as paths from 'vs/base/common/paths'; -import * as extfs from 'vs/base/node/extfs'; -import * as encoding from 'vs/base/node/encoding'; -import * as glob from 'vs/base/common/glob'; import { TPromise } from 'vs/base/common/winjs.base'; - -import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, IFolderSearch, LineMatch, FileMatch } from './search'; -import { IProgress } from 'vs/platform/search/common/search'; +import * as encoding from 'vs/base/node/encoding'; +import * as extfs from 'vs/base/node/extfs'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IProgress, ITextSearchPreviewOptions, ITextSearchStats, TextSearchResult } from 'vs/platform/search/common/search'; +import { rgPath } from 'vscode-ripgrep'; +import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, ISerializedSearchSuccess } from './search'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); @@ -47,12 +45,15 @@ export class RipgrepEngine { } // TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore - search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { if (!this.config.folderQueries.length && !this.config.extraFiles.length) { process.removeListener('exit', this.killRgProcFn); done(null, { + type: 'success', limitHit: false, - stats: null + stats: { + type: 'searchProcess' + } }); return; } @@ -63,25 +64,24 @@ export class RipgrepEngine { } const cwd = platform.isWindows ? 'c:/' : '/'; - process.nextTick(() => { // Allow caller to register progress callback - const escapedArgs = rgArgs.args - .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) - .join(' '); + const escapedArgs = rgArgs.args + .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) + .join(' '); - let rgCmd = `rg ${escapedArgs}\n - cwd: ${cwd}`; - if (rgArgs.siblingClauses) { - rgCmd += `\n - Sibling clauses: ${JSON.stringify(rgArgs.siblingClauses)}`; - } + let rgCmd = `rg ${escapedArgs}\n - cwd: ${cwd}`; + if (rgArgs.siblingClauses) { + rgCmd += `\n - Sibling clauses: ${JSON.stringify(rgArgs.siblingClauses)}`; + } + + onMessage({ message: rgCmd }); - onMessage({ message: rgCmd }); - }); this.rgProc = cp.spawn(rgDiskPath, rgArgs.args, { cwd }); process.once('exit', this.killRgProcFn); - this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles); + this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles, this.config.previewOptions); this.ripgrepParser.on('result', (match: ISerializedFileMatch) => { if (this.postProcessExclusions) { - const handleResultP = (>this.postProcessExclusions(match.path, undefined, () => getSiblings(match.path))) + const handleResultP = (>this.postProcessExclusions(match.path, undefined, glob.hasSiblingPromiseFn(() => getSiblings(match.path)))) .then(globMatch => { if (!globMatch) { onResult(match); @@ -97,8 +97,11 @@ export class RipgrepEngine { this.cancel(); process.removeListener('exit', this.killRgProcFn); done(null, { + type: 'success', limitHit: true, - stats: null + stats: { + type: 'searchProcess' + } }); }); @@ -127,11 +130,13 @@ export class RipgrepEngine { process.removeListener('exit', this.killRgProcFn); if (stderr && !gotData && (displayMsg = rgErrorMsgForDisplay(stderr))) { done(new Error(displayMsg), { + type: 'success', limitHit: false, stats: null }); } else { done(null, { + type: 'success', limitHit: false, stats: null }); @@ -148,20 +153,30 @@ export class RipgrepEngine { * "failed" when a fatal error was produced. */ export function rgErrorMsgForDisplay(msg: string): string | undefined { - const firstLine = msg.split('\n')[0]; + const lines = msg.trim().split('\n'); + const firstLine = lines[0].trim(); if (strings.startsWith(firstLine, 'Error parsing regex')) { return firstLine; } + if (strings.startsWith(firstLine, 'regex parse error')) { + return strings.uppercaseFirstLetter(lines[lines.length - 1].trim()); + } + if (strings.startsWith(firstLine, 'error parsing glob') || strings.startsWith(firstLine, 'unsupported encoding')) { // Uppercase first letter return firstLine.charAt(0).toUpperCase() + firstLine.substr(1); } + if (firstLine === `Literal '\\n' not allowed.`) { + // I won't localize this because none of the Ripgrep error messages are localized + return `Literal '\\n' currently not supported`; + } + if (strings.startsWith(firstLine, 'Literal ')) { - // e.g. "Literal \n not allowed" + // Other unsupported chars return firstLine; } @@ -183,7 +198,7 @@ export class RipgrepParser extends EventEmitter { private numResults = 0; - constructor(private maxResults: number, private rootFolder: string, extraFiles?: string[]) { + constructor(private maxResults: number, private rootFolder: string, extraFiles?: string[], private previewOptions?: ITextSearchPreviewOptions) { super(); this.stringDecoder = new StringDecoder(); @@ -261,7 +276,6 @@ export class RipgrepParser extends EventEmitter { text = strings.stripUTF8BOM(text); } - const lineMatch = new LineMatch(text, lineNum); if (!this.fileMatch) { // When searching a single file and no folderQueries, rg does not print the file line, so create it here const singleFile = this.extraSearchFiles[0]; @@ -272,8 +286,6 @@ export class RipgrepParser extends EventEmitter { this.fileMatch = this.getFileMatch(singleFile); } - this.fileMatch.addMatch(lineMatch); - let lastMatchEndPos = 0; let matchTextStartPos = -1; @@ -282,6 +294,7 @@ export class RipgrepParser extends EventEmitter { let textRealIdx = 0; let hitLimit = false; + const matchRanges: IRange[] = []; const realTextParts: string[] = []; for (let i = 0; i < text.length - (RipgrepParser.MATCH_END_MARKER.length - 1);) { @@ -297,7 +310,7 @@ export class RipgrepParser extends EventEmitter { const chunk = text.slice(matchTextStartPos, i); realTextParts.push(chunk); if (!hitLimit) { - lineMatch.addMatch(matchTextStartRealIdx, textRealIdx - matchTextStartRealIdx); + matchRanges.push(new Range(lineNum, matchTextStartRealIdx, lineNum, textRealIdx)); } matchTextStartPos = -1; @@ -322,7 +335,9 @@ export class RipgrepParser extends EventEmitter { // Replace preview with version without color codes const preview = realTextParts.join(''); - lineMatch.preview = preview; + matchRanges + .map(r => new TextSearchResult(preview, r, this.previewOptions)) + .forEach(m => this.fileMatch.addMatch(m)); if (hitLimit) { this.cancel(); @@ -496,6 +511,7 @@ function getRgArgs(config: IRawSearch) { } args.push('--no-config'); + args.push('--no-ignore-global'); // Folder to search args.push('--'); diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index a1061c52d22..e3f65aa8fbe 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -5,9 +5,10 @@ 'use strict'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; import { IExpression } from 'vs/base/common/glob'; -import { IProgress, ILineMatch, IPatternInfo, ISearchStats } from 'vs/platform/search/common/search'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IFileSearchStats, IPatternInfo, IProgress, ISearchEngineStats, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats } from 'vs/platform/search/common/search'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; export interface IFolderSearch { @@ -33,6 +34,7 @@ export interface IRawSearch { maxFilesize?: number; useRipgrep?: boolean; disregardIgnoreFiles?: boolean; + previewOptions?: ITextSearchPreviewOptions; } export interface ITelemetryEvent { @@ -41,10 +43,9 @@ export interface ITelemetryEvent { } export interface IRawSearchService { - fileSearch(search: IRawSearch): PPromise; - textSearch(search: IRawSearch): PPromise; + fileSearch(search: IRawSearch): Event; + textSearch(search: IRawSearch): Event; clearCache(cacheKey: string): TPromise; - fetchTelemetry(): PPromise; } export interface IRawFileMatch { @@ -55,18 +56,48 @@ export interface IRawFileMatch { } export interface ISearchEngine { - search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void; + search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISearchEngineSuccess) => void) => void; cancel: () => void; } -export interface ISerializedSearchComplete { +export interface ISerializedSearchSuccess { + type: 'success'; limitHit: boolean; - stats: ISearchStats; + stats: IFileSearchStats | ITextSearchStats; +} + +export interface ISearchEngineSuccess { + limitHit: boolean; + stats: ISearchEngineStats; +} + +export interface ISerializedSearchError { + type: 'error'; + error: { + message: string, + stack: string + }; +} + +export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError; + +export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete { + if ((arg as any).type === 'error') { + return true; + } else if ((arg as any).type === 'success') { + return true; + } else { + return false; + } +} + +export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess { + return arg.type === 'success'; } export interface ISerializedFileMatch { path: string; - lineMatches?: ILineMatch[]; + matches?: ITextSearchResult[]; numMatches?: number; } @@ -77,56 +108,22 @@ export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgres export class FileMatch implements ISerializedFileMatch { path: string; - lineMatches: LineMatch[]; + matches: ITextSearchResult[]; constructor(path: string) { this.path = path; - this.lineMatches = []; + this.matches = []; } - addMatch(lineMatch: LineMatch): void { - this.lineMatches.push(lineMatch); + addMatch(match: ITextSearchResult): void { + this.matches.push(match); } serialize(): ISerializedFileMatch { - let lineMatches: ILineMatch[] = []; - let numMatches = 0; - - for (let i = 0; i < this.lineMatches.length; i++) { - numMatches += this.lineMatches[i].offsetAndLengths.length; - lineMatches.push(this.lineMatches[i].serialize()); - } - return { path: this.path, - lineMatches, - numMatches + matches: this.matches, + numMatches: this.matches.length }; } } - -export class LineMatch implements ILineMatch { - preview: string; - lineNumber: number; - offsetAndLengths: number[][]; - - constructor(preview: string, lineNumber: number) { - this.preview = preview.replace(/(\r|\n)*$/, ''); - this.lineNumber = lineNumber; - this.offsetAndLengths = []; - } - - addMatch(offset: number, length: number): void { - this.offsetAndLengths.push([offset, length]); - } - - serialize(): ILineMatch { - const result = { - preview: this.preview, - lineNumber: this.lineNumber, - offsetAndLengths: this.offsetAndLengths - }; - - return result; - } -} \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/searchIpc.ts b/src/vs/workbench/services/search/node/searchIpc.ts index 4e9575e0d55..1cc62837237 100644 --- a/src/vs/workbench/services/search/node/searchIpc.ts +++ b/src/vs/workbench/services/search/node/searchIpc.ts @@ -5,15 +5,15 @@ 'use strict'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search'; +import { Event } from 'vs/base/common/event'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; +import { IRawSearch, IRawSearchService, ISerializedSearchComplete, ISerializedSearchProgressItem } from './search'; export interface ISearchChannel extends IChannel { - call(command: 'fileSearch', search: IRawSearch): PPromise; - call(command: 'textSearch', search: IRawSearch): PPromise; + listen(event: 'fileSearch', search: IRawSearch): Event; + listen(event: 'textSearch', search: IRawSearch): Event; call(command: 'clearCache', cacheKey: string): TPromise; - call(command: 'fetchTelemetry'): PPromise; call(command: string, arg: any): TPromise; } @@ -21,14 +21,19 @@ export class SearchChannel implements ISearchChannel { constructor(private service: IRawSearchService) { } - call(command: string, arg?: any): TPromise { - switch (command) { + listen(event: string, arg?: any): Event { + switch (event) { case 'fileSearch': return this.service.fileSearch(arg); case 'textSearch': return this.service.textSearch(arg); - case 'clearCache': return this.service.clearCache(arg); - case 'fetchTelemetry': return this.service.fetchTelemetry(); } - return undefined; + throw new Error('Event not found'); + } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'clearCache': return this.service.clearCache(arg); + } + throw new Error('Call not found'); } } @@ -36,19 +41,15 @@ export class SearchChannelClient implements IRawSearchService { constructor(private channel: ISearchChannel) { } - fileSearch(search: IRawSearch): PPromise { - return this.channel.call('fileSearch', search); + fileSearch(search: IRawSearch): Event { + return this.channel.listen('fileSearch', search); } - textSearch(search: IRawSearch): PPromise { - return this.channel.call('textSearch', search); + textSearch(search: IRawSearch): Event { + return this.channel.listen('textSearch', search); } clearCache(cacheKey: string): TPromise { return this.channel.call('clearCache', cacheKey); } - - fetchTelemetry(): PPromise { - return this.channel.call('fetchTelemetry'); - } } \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index f9bb3b703fe..0109bd5dc89 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -4,58 +4,73 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import uri from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; import * as arrays from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap, values } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import * as objects from 'vs/base/common/objects'; +import { StopWatch } from 'vs/base/common/stopwatch'; import * as strings from 'vs/base/common/strings'; -import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; +import { URI as uri } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as pfs from 'vs/base/node/pfs'; +import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; -import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, IFolderQuery, ISearchConfiguration, ISearchService, pathIncludedInQuery, ISearchResultProvider } from 'vs/platform/search/common/search'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { Range } from 'vs/editor/common/core/range'; +import { FindMatch, ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent } from './search'; -import { ISearchChannel, SearchChannelClient } from './searchIpc'; -import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment'; -import { ResourceMap } from 'vs/base/common/map'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { Schemas } from 'vs/base/common/network'; -import * as pfs from 'vs/base/node/pfs'; +import { IDebugParams, IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; +import { FileMatch, ICachedSearchStats, IFileMatch, IFileSearchStats, IFolderQuery, IProgress, ISearchComplete, ISearchConfiguration, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType, SearchProviderType, TextSearchResult } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IRawSearch, IRawSearchService, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess } from './search'; +import { ISearchChannel, SearchChannelClient } from './searchIpc'; -export class SearchService implements ISearchService { +export class SearchService extends Disposable implements ISearchService { public _serviceBrand: any; private diskSearch: DiskSearch; - private readonly searchProviders: ISearchResultProvider[] = []; - private forwardingTelemetry: PPromise; + private readonly fileSearchProviders = new Map(); + private readonly textSearchProviders = new Map(); + private readonly fileIndexProviders = new Map(); constructor( @IModelService private modelService: IModelService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, + @IEditorService private editorService: IEditorService, @IEnvironmentService environmentService: IEnvironmentService, @ITelemetryService private telemetryService: ITelemetryService, @IConfigurationService private configurationService: IConfigurationService, @ILogService private logService: ILogService, @IExtensionService private extensionService: IExtensionService ) { + super(); this.diskSearch = new DiskSearch(!environmentService.isBuilt || environmentService.verbose, /*timeout=*/undefined, environmentService.debugSearch); } - public registerSearchResultProvider(provider: ISearchResultProvider): IDisposable { - this.searchProviders.push(provider); - return { - dispose: () => { - const idx = this.searchProviders.indexOf(provider); - if (idx >= 0) { - this.searchProviders.splice(idx, 1); - } - } - }; + public registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable { + let list: Map; + if (type === SearchProviderType.file) { + list = this.fileSearchProviders; + } else if (type === SearchProviderType.text) { + list = this.textSearchProviders; + } else if (type === SearchProviderType.fileIndex) { + list = this.fileIndexProviders; + } + + list.set(scheme, provider); + + return toDisposable(() => { + list.delete(scheme); + }); } public extendQuery(query: ISearchQuery): void { @@ -80,101 +95,228 @@ export class SearchService implements ISearchService { } } - public search(query: ISearchQuery): PPromise { - this.forwardTelemetry(); + public search(query: ISearchQuery, token?: CancellationToken, onProgress?: (item: ISearchProgressItem) => void): TPromise { + // Get local results from dirty/untitled + const localResults = this.getLocalResults(query); - let combinedPromise: TPromise; + if (onProgress) { + localResults.values().filter((res) => !!res).forEach(onProgress); + } - return new PPromise((onComplete, onError, onProgress) => { + this.logService.trace('SearchService#search', JSON.stringify(query)); - // Get local results from dirty/untitled - const localResults = this.getLocalResults(query); - - // Allow caller to register progress callback - process.nextTick(() => localResults.values().filter((res) => !!res).forEach(onProgress)); - - this.logService.trace('SearchService#search', JSON.stringify(query)); - - const startTime = Date.now(); - const searchWithProvider = (provider: ISearchResultProvider) => TPromise.wrap(provider.search(query)).then(e => e, - null, - progress => { - if (progress.resource) { - // Match - if (!localResults.has(progress.resource)) { // don't override local results - onProgress(progress); - } - } else { - // Progress - onProgress(progress); - } - - if (progress.message) { - this.logService.debug('SearchService#search', progress.message); - } - }); - - const providerPromise = this.extensionService.whenInstalledExtensionsRegistered().then(() => { - // If no search providers are registered, fall back on DiskSearch - // TODO@roblou this is not properly waiting for search-rg to finish registering itself - if (this.searchProviders.length) { - return TPromise.join(this.searchProviders.map(p => searchWithProvider(p))) - .then(completes => { - completes = completes.filter(c => !!c); - if (!completes.length) { - return null; - } - - return { - limitHit: completes[0] && completes[0].limitHit, - stats: completes[0].stats, - results: arrays.flatten(completes.map(c => c.results)) - }; - }, errs => { - if (!Array.isArray(errs)) { - errs = [errs]; - } - - errs = errs.filter(e => !!e); - return TPromise.wrapError(errs[0]); - }); - } else { - return searchWithProvider(this.diskSearch); + const onProviderProgress = progress => { + if (progress.resource) { + // Match + if (!localResults.has(progress.resource) && onProgress) { // don't override local results + onProgress(progress); } + } else if (onProgress) { + // Progress + onProgress(progress); + } + + if (progress.message) { + this.logService.debug('SearchService#search', progress.message); + } + }; + + const schemesInQuery = this.getSchemesInQuery(query); + + const providerActivations: TPromise[] = [TPromise.wrap(null)]; + schemesInQuery.forEach(scheme => providerActivations.push(this.extensionService.activateByEvent(`onSearch:${scheme}`))); + + const providerPromise = TPromise.join(providerActivations) + .then(() => this.extensionService.whenInstalledExtensionsRegistered()) + .then(() => this.searchWithProviders(query, onProviderProgress, token)) + .then(completes => { + completes = completes.filter(c => !!c); + if (!completes.length) { + return null; + } + + return { + limitHit: completes[0] && completes[0].limitHit, + stats: completes[0].stats, + results: arrays.flatten(completes.map(c => c.results)) + }; + }, errs => { + if (!Array.isArray(errs)) { + errs = [errs]; + } + + errs = errs.filter(e => !!e); + return TPromise.wrapError(errs[0]); }); - combinedPromise = providerPromise.then(value => { - this.logService.debug(`SearchService#search: ${Date.now() - startTime}ms`); - const values = [value]; + return providerPromise.then(value => { + const values = [value]; - const result: ISearchComplete = { - limitHit: false, - results: [], - stats: undefined - }; + const result: ISearchComplete = { + limitHit: false, + results: [], + stats: undefined + }; - // TODO@joh - // sorting, disjunct results - for (const value of values) { - if (!value) { - continue; - } - // TODO@joh individual stats/limit - result.stats = value.stats || result.stats; - result.limitHit = value.limitHit || result.limitHit; + // TODO@joh + // sorting, disjunct results + for (const value of values) { + if (!value) { + continue; + } + // TODO@joh individual stats/limit + result.stats = value.stats || result.stats; + result.limitHit = value.limitHit || result.limitHit; - for (const match of value.results) { - if (!localResults.has(match.resource)) { - result.results.push(match); - } + for (const match of value.results) { + if (!localResults.has(match.resource)) { + result.results.push(match); } } + } - return result; + return result; + }); - }).then(onComplete, onError); + } - }, () => combinedPromise && combinedPromise.cancel()); + private getSchemesInQuery(query: ISearchQuery): Set { + const schemes = new Set(); + if (query.folderQueries) { + query.folderQueries.forEach(fq => schemes.add(fq.folder.scheme)); + } + + if (query.extraFileResources) { + query.extraFileResources.forEach(extraFile => schemes.add(extraFile.scheme)); + } + + return schemes; + } + + private searchWithProviders(query: ISearchQuery, onProviderProgress: (progress: ISearchProgressItem) => void, token?: CancellationToken) { + const e2eSW = StopWatch.create(false); + + const diskSearchQueries: IFolderQuery[] = []; + const searchPs: TPromise[] = []; + + query.folderQueries.forEach(fq => { + let provider = query.type === QueryType.File ? + this.fileSearchProviders.get(fq.folder.scheme) || this.fileIndexProviders.get(fq.folder.scheme) : + this.textSearchProviders.get(fq.folder.scheme); + + if (!provider && fq.folder.scheme === 'file') { + diskSearchQueries.push(fq); + } else if (!provider) { + throw new Error('No search provider registered for scheme: ' + fq.folder.scheme); + } else { + const oneFolderQuery = { + ...query, + ...{ + folderQueries: [fq] + } + }; + + searchPs.push(provider.search(oneFolderQuery, onProviderProgress, token)); + } + }); + + const diskSearchExtraFileResources = query.extraFileResources && query.extraFileResources.filter(res => res.scheme === 'file'); + + if (diskSearchQueries.length || diskSearchExtraFileResources) { + const diskSearchQuery: ISearchQuery = { + ...query, + ...{ + folderQueries: diskSearchQueries + }, + extraFileResources: diskSearchExtraFileResources + }; + + searchPs.push(this.diskSearch.search(diskSearchQuery, onProviderProgress, token)); + } + + return TPromise.join(searchPs).then(completes => { + const endToEndTime = e2eSW.elapsed(); + this.logService.trace(`SearchService#search: ${endToEndTime}ms`); + completes.forEach(complete => { + this.sendTelemetry(query, endToEndTime, complete); + }); + return completes; + }); + } + + private sendTelemetry(query: ISearchQuery, endToEndTime: number, complete: ISearchComplete): void { + const fileSchemeOnly = query.folderQueries.every(fq => fq.folder.scheme === 'file'); + const otherSchemeOnly = query.folderQueries.every(fq => fq.folder.scheme !== 'file'); + const scheme = fileSchemeOnly ? 'file' : + otherSchemeOnly ? 'other' : + 'mixed'; + + if (query.type === QueryType.File && complete.stats) { + const fileSearchStats = complete.stats as IFileSearchStats; + if (fileSearchStats.fromCache) { + const cacheStats: ICachedSearchStats = fileSearchStats.detailStats as ICachedSearchStats; + + /* __GDPR__ + "cachedSearchComplete" : { + "resultCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "workspaceFolderCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "endToEndTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "sortingTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "cacheWasResolved" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cacheLookupTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "cacheFilterTime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "cacheEntryCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + } + */ + this.telemetryService.publicLog('cachedSearchComplete', { + resultCount: fileSearchStats.resultCount, + workspaceFolderCount: query.folderQueries.length, + type: fileSearchStats.type, + endToEndTime: endToEndTime, + sortingTime: fileSearchStats.sortingTime, + cacheWasResolved: cacheStats.cacheWasResolved, + cacheLookupTime: cacheStats.cacheLookupTime, + cacheFilterTime: cacheStats.cacheFilterTime, + cacheEntryCount: cacheStats.cacheEntryCount, + scheme + }); + } else { + const searchEngineStats: ISearchEngineStats = fileSearchStats.detailStats as ISearchEngineStats; + + /* __GDPR__ + "searchComplete" : { + "resultCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "workspaceFolderCount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "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" }, + } + */ + this.telemetryService.publicLog('searchComplete', { + resultCount: fileSearchStats.resultCount, + workspaceFolderCount: query.folderQueries.length, + 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 + }); + } + } } private getLocalResults(query: ISearchQuery): ResourceMap { @@ -188,6 +330,10 @@ export class SearchService implements ISearchService { return; } + if (!this.editorService.isOpen({ resource })) { + return; + } + // Support untitled files if (resource.scheme === Schemas.untitled) { if (!this.untitledEditorService.exists(resource)) { @@ -214,7 +360,7 @@ export class SearchService implements ISearchService { localResults.set(resource, fileMatch); matches.forEach((match) => { - fileMatch.lineMatches.push(new LineMatch(model.getLineContent(match.range.startLineNumber), match.range.startLineNumber - 1, [[match.range.startColumn - 1, match.range.endColumn - match.range.startColumn]])); + fileMatch.matches.push(editorMatchToTextSearchResult(match, model, query.previewOptions)); }); } else { localResults.set(resource, null); @@ -248,16 +394,13 @@ export class SearchService implements ISearchService { } public clearCache(cacheKey: string): TPromise { - return this.diskSearch.clearCache(cacheKey); - } + const clearPs = [ + this.diskSearch, + ...values(this.fileIndexProviders) + ].map(provider => provider && provider.clearCache(cacheKey)); - private forwardTelemetry() { - if (!this.forwardingTelemetry) { - this.forwardingTelemetry = this.diskSearch.fetchTelemetry() - .then(null, onUnexpectedError, event => { - this.telemetryService.publicLog(event.eventName, event.data); - }); - } + return TPromise.join(clearPs) + .then(() => { }); } } @@ -291,28 +434,28 @@ export class DiskSearch implements ISearchResultProvider { } const client = new Client( - uri.parse(require.toUrl('bootstrap')).fsPath, + getPathFromAmdModule(require, 'bootstrap-fork'), opts); const channel = getNextTickChannel(client.getChannel('search')); this.raw = new SearchChannelClient(channel); } - public search(query: ISearchQuery): PPromise { + public search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise { const folderQueries = query.folderQueries || []; return TPromise.join(folderQueries.map(q => q.folder.scheme === Schemas.file && pfs.exists(q.folder.fsPath))) .then(exists => { const existingFolders = folderQueries.filter((q, index) => exists[index]); const rawSearch = this.rawSearchQuery(query, existingFolders); - let request: PPromise; + let event: Event; if (query.type === QueryType.File) { - request = this.raw.fileSearch(rawSearch); + event = this.raw.fileSearch(rawSearch); } else { - request = this.raw.textSearch(rawSearch); + event = this.raw.textSearch(rawSearch); } - return DiskSearch.collectResults(request); + return DiskSearch.collectResultsFromEvent(event, onProgress, token); }); } @@ -329,7 +472,8 @@ export class DiskSearch implements ISearchResultProvider { cacheKey: query.cacheKey, useRipgrep: query.useRipgrep, disregardIgnoreFiles: query.disregardIgnoreFiles, - ignoreSymlinks: query.ignoreSymlinks + ignoreSymlinks: query.ignoreSymlinks, + previewOptions: query.previewOptions }; for (const q of existingFolders) { @@ -357,45 +501,67 @@ export class DiskSearch implements ISearchResultProvider { return rawSearch; } - public static collectResults(request: PPromise): PPromise { + public static collectResultsFromEvent(event: Event, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise { let result: IFileMatch[] = []; - return new PPromise((c, e, p) => { - request.done((complete) => { - c({ - limitHit: complete.limitHit, - results: result, - stats: complete.stats + + let listener: IDisposable; + return new TPromise((c, e) => { + if (token) { + token.onCancellationRequested(() => { + if (listener) { + listener.dispose(); + } + + e(canceled()); }); - }, e, (data) => { + } - // Matches - if (Array.isArray(data)) { - const fileMatches = data.map(d => this.createFileMatch(d)); - result = result.concat(fileMatches); - fileMatches.forEach(p); - } + listener = event(ev => { + if (isSerializedSearchComplete(ev)) { + if (isSerializedSearchSuccess(ev)) { + c({ + limitHit: ev.limitHit, + results: result, + stats: ev.stats + }); + } else { + e(ev.error); + } - // Match - else if ((data).path) { - const fileMatch = this.createFileMatch(data); - result.push(fileMatch); - p(fileMatch); - } + listener.dispose(); + } else { + // Matches + if (Array.isArray(ev)) { + const fileMatches = ev.map(d => this.createFileMatch(d)); + result = result.concat(fileMatches); + if (onProgress) { + fileMatches.forEach(onProgress); + } + } - // Progress - else { - p(data); + // Match + else if ((ev).path) { + const fileMatch = this.createFileMatch(ev); + result.push(fileMatch); + + if (onProgress) { + onProgress(fileMatch); + } + } + + // Progress + else if (onProgress) { + onProgress(ev); + } } }); - }, () => request.cancel()); + }); } private static createFileMatch(data: ISerializedFileMatch): FileMatch { let fileMatch = new FileMatch(uri.file(data.path)); - if (data.lineMatches) { - for (let j = 0; j < data.lineMatches.length; j++) { - fileMatch.lineMatches.push(new LineMatch(data.lineMatches[j].preview, data.lineMatches[j].lineNumber, data.lineMatches[j].offsetAndLengths)); - } + if (data.matches) { + fileMatch.matches.push(...data.matches); // TODO why } return fileMatch; } @@ -403,8 +569,21 @@ export class DiskSearch implements ISearchResultProvider { public clearCache(cacheKey: string): TPromise { return this.raw.clearCache(cacheKey); } - - public fetchTelemetry(): PPromise { - return this.raw.fetchTelemetry(); - } +} + +/** + * While search doesn't support multiline matches, collapse editor matches to a single line + */ +function editorMatchToTextSearchResult(match: FindMatch, model: ITextModel, previewOptions: ITextSearchPreviewOptions): TextSearchResult { + let endLineNumber = match.range.endLineNumber - 1; + let endCol = match.range.endColumn - 1; + if (match.range.endLineNumber !== match.range.startLineNumber) { + endLineNumber = match.range.startLineNumber - 1; + endCol = model.getLineLength(match.range.startLineNumber); + } + + return new TextSearchResult( + model.getLineContent(match.range.startLineNumber), + new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, endLineNumber, endCol), + previewOptions); } diff --git a/src/vs/workbench/services/search/node/textSearch.ts b/src/vs/workbench/services/search/node/textSearch.ts index c2002bb88ed..02b15dafa4a 100644 --- a/src/vs/workbench/services/search/node/textSearch.ts +++ b/src/vs/workbench/services/search/node/textSearch.ts @@ -6,14 +6,12 @@ 'use strict'; import * as path from 'path'; - import { onUnexpectedError } from 'vs/base/common/errors'; import { IProgress } from 'vs/platform/search/common/search'; import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; - -import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search'; -import { ISearchWorker } from './worker/searchWorkerIpc'; +import { IRawSearch, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch } from './search'; import { ITextSearchWorkerProvider } from './textSearchWorkerProvider'; +import { ISearchWorker, ISearchWorkerSearchArgs } from './worker/searchWorkerIpc'; export class Engine implements ISearchEngine { @@ -60,7 +58,7 @@ export class Engine implements ISearchEngine { }); } - search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISearchEngineSuccess) => void): void { this.workers = this.workerProvider.getWorkers(); this.initializeWorkers(); @@ -97,7 +95,7 @@ export class Engine implements ISearchEngine { this.nextWorker = (this.nextWorker + 1) % this.workers.length; const maxResults = this.config.maxResults && (this.config.maxResults - this.numResults); - const searchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding }; + 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); diff --git a/src/vs/workbench/services/search/node/textSearchWorkerProvider.ts b/src/vs/workbench/services/search/node/textSearchWorkerProvider.ts index 1b7b03ef9a7..f4d7855f295 100644 --- a/src/vs/workbench/services/search/node/textSearchWorkerProvider.ts +++ b/src/vs/workbench/services/search/node/textSearchWorkerProvider.ts @@ -7,11 +7,11 @@ import * as os from 'os'; -import uri from 'vs/base/common/uri'; -import * as ipc from 'vs/base/parts/ipc/common/ipc'; +import * as ipc from 'vs/base/parts/ipc/node/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { ISearchWorker, ISearchWorkerChannel, SearchWorkerChannelClient } from './worker/searchWorkerIpc'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export interface ITextSearchWorkerProvider { getWorkers(): ISearchWorker[]; @@ -31,7 +31,7 @@ export class TextSearchWorkerProvider implements ITextSearchWorkerProvider { private createWorker(): void { let client = new Client( - uri.parse(require.toUrl('bootstrap')).fsPath, + getPathFromAmdModule(require, 'bootstrap-fork'), { serverName: 'Search Worker ' + this.workers.length, args: ['--type=searchWorker'], @@ -49,4 +49,4 @@ export class TextSearchWorkerProvider implements ITextSearchWorkerProvider { this.workers.push(channelClient); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/search/node/worker/searchWorker.ts b/src/vs/workbench/services/search/node/worker/searchWorker.ts index cdda1b2b257..b1e340ac07b 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorker.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorker.ts @@ -7,16 +7,17 @@ import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); - import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; -import { LineMatch, FileMatch } from '../search'; -import { UTF16le, UTF16be, UTF8, UTF8_with_bom, encodingExists, decode, bomLength, detectEncodingFromBuffer } from 'vs/base/node/encoding'; - +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, TextSearchResult } from 'vs/platform/search/common/search'; +import { FileMatch } from '../search'; import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc'; +gracefulFs.gracefulify(fs); + interface ReadLinesOptions { bufferLength: number; encoding: string; @@ -95,7 +96,7 @@ export class SearchWorkerEngine { // Search in the given path, and when it's finished, search in the next path in absolutePaths const startSearchInFile = (absolutePath: string): TPromise => { - return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches)).then(fileResult => { + 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; @@ -124,13 +125,12 @@ export class SearchWorkerEngine { this.isCanceled = true; } - private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number): TPromise { + private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number, previewOptions?: ITextSearchPreviewOptions): TPromise { let fileMatch: FileMatch = null; let limitReached = false; let numMatches = 0; const perLineCallback = (line: string, lineNumber: number) => { - let lineMatch: LineMatch = null; let match = contentPattern.exec(line); // Record all matches into file result @@ -139,12 +139,8 @@ export class SearchWorkerEngine { fileMatch = new FileMatch(absolutePath); } - if (lineMatch === null) { - lineMatch = new LineMatch(line, lineNumber); - fileMatch.addMatch(lineMatch); - } - - lineMatch.addMatch(match.index, match[0].length); + const lineMatch = new TextSearchResult(line, new Range(lineNumber, match.index, lineNumber, match.index + match[0].length), previewOptions); + fileMatch.addMatch(lineMatch); numMatches++; if (maxResults && numMatches >= maxResults) { @@ -177,8 +173,8 @@ export class SearchWorkerEngine { return clb(null); // return early if canceled or limit reached } - fs.read(fd, buffer, 0, buffer.length, null, (error: Error, bytesRead: number, buffer: NodeBuffer) => { - const decodeBuffer = (buffer: NodeBuffer, start: number, end: number): string => { + 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 } diff --git a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts index 5260dc0b80d..5fed1210eec 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts @@ -6,16 +6,18 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel } from 'vs/base/parts/ipc/node/ipc'; import { ISerializedFileMatch } from '../search'; -import { IPatternInfo } from 'vs/platform/search/common/search'; +import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search'; import { SearchWorker } from './searchWorker'; +import { Event } from 'vs/base/common/event'; export interface ISearchWorkerSearchArgs { pattern: IPatternInfo; fileEncoding: string; absolutePaths: string[]; maxResults?: number; + previewOptions?: ITextSearchPreviewOptions; } export interface ISearchWorkerSearchResult { @@ -41,6 +43,10 @@ export class SearchWorkerChannel implements ISearchWorkerChannel { constructor(private worker: SearchWorker) { } + listen(event: string, arg?: any): Event { + throw new Error('No events'); + } + call(command: string, arg?: any): TPromise { switch (command) { case 'initialize': return this.worker.initialize(); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts index ac23cfa0c7e..8e2f789924c 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts @@ -5,16 +5,13 @@ 'use strict'; -import * as path from 'path'; import * as assert from 'assert'; - +import * as path from 'path'; import * as arrays from 'vs/base/common/arrays'; import * as platform from 'vs/base/common/platform'; - -import { RipgrepParser, getAbsoluteGlob, fixDriveC, fixRegexEndingPattern } from 'vs/workbench/services/search/node/ripgrepTextSearch'; +import { fixDriveC, fixRegexEndingPattern, getAbsoluteGlob, RipgrepParser } from 'vs/workbench/services/search/node/ripgrepTextSearch'; import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search'; - suite('RipgrepParser', () => { const rootFolder = '/workspace'; const fileSectionEnd = '\n'; @@ -76,16 +73,40 @@ suite('RipgrepParser', () => { { numMatches: 2, path: path.join(rootFolder, 'a.txt'), - lineMatches: [ + matches: [ { - lineNumber: 0, - preview: 'beforematchafter', - offsetAndLengths: [[6, 5]] + preview: { + match: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + }, + text: 'beforematchafter' + }, + range: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + } }, { - lineNumber: 1, - preview: 'beforematchafter', - offsetAndLengths: [[6, 5]] + preview: { + match: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + }, + text: 'beforematchafter' + }, + range: { + endColumn: 11, + endLineNumber: 1, + startColumn: 6, + startLineNumber: 1, + } } ] }); @@ -157,7 +178,7 @@ suite('RipgrepParser', () => { test('Parses chunks broken in the middle of a multibyte character', () => { const text = getFileLine('foo/bar') + '\n' + getMatchLine(0, ['before漢', 'match', 'after']) + '\n'; - const buf = new Buffer(text); + const buf = Buffer.from(text); // Split the buffer at every possible position - it should still be parsed correctly for (let i = 0; i < buf.length; i++) { @@ -168,8 +189,9 @@ suite('RipgrepParser', () => { const results = parseInput(inputBufs); assert.equal(results.length, 1); - assert.equal(results[0].lineMatches.length, 1); - assert.deepEqual(results[0].lineMatches[0].offsetAndLengths, [[7, 5]]); + assert.equal(results[0].matches.length, 1); + assert.equal(results[0].matches[0].range.startColumn, 7); + assert.equal(results[0].matches[0].range.endColumn, 12); } }); }); 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 c4a69c77005..7e8ded6fe5d 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -13,8 +13,9 @@ import * as platform from 'vs/base/common/platform'; import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; import { IRawFileMatch, IFolderSearch } from 'vs/workbench/services/search/node/search'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; -const TEST_FIXTURES = path.normalize(require.toUrl('./fixtures')); +const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures')); const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples'); const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more'); const TEST_ROOT_FOLDER: IFolderSearch = { folder: TEST_FIXTURES }; @@ -23,7 +24,7 @@ const ROOT_FOLDER_QUERY: IFolderSearch[] = [ ]; const ROOT_FOLDER_QUERY_36438: IFolderSearch[] = [ - { folder: path.normalize(require.toUrl('./fixtures2/36438')) } + { folder: path.normalize(getPathFromAmdModule(require, './fixtures2/36438')) } ]; const MULTIROOT_QUERIES: IFolderSearch[] = [ @@ -578,29 +579,6 @@ suite('FileSearchEngine', () => { }); }); - test('Files: absolute path to file ignores excludes', function (done: () => void) { - this.timeout(testTimeout); - let engine = new FileSearchEngine({ - folderQueries: ROOT_FOLDER_QUERY, - filePattern: path.normalize(path.join(require.toUrl('./fixtures'), 'site.css')), - excludePattern: { '**/*.css': true } - }); - - let count = 0; - let res: IRawFileMatch; - engine.search((result) => { - if (result) { - count++; - } - res = result; - }, () => { }, (error) => { - assert.ok(!error); - assert.equal(count, 1); - assert.equal(path.basename(res.relativePath), 'site.css'); - done(); - }); - }); - test('Files: relative path matched once', function (done: () => void) { this.timeout(testTimeout); let engine = new FileSearchEngine({ @@ -623,29 +601,6 @@ suite('FileSearchEngine', () => { }); }); - test('Files: relative path to file ignores excludes', function (done: () => void) { - this.timeout(testTimeout); - let engine = new FileSearchEngine({ - folderQueries: ROOT_FOLDER_QUERY, - filePattern: path.normalize(path.join('examples', 'company.js')), - excludePattern: { '**/*.js': true } - }); - - let count = 0; - let res: IRawFileMatch; - engine.search((result) => { - if (result) { - count++; - } - res = result; - }, () => { }, (error) => { - assert.ok(!error); - assert.equal(count, 1); - assert.equal(path.basename(res.relativePath), 'company.js'); - done(); - }); - }); - test('Files: Include pattern, single files', function (done: () => void) { this.timeout(testTimeout); let engine = new FileSearchEngine({ @@ -675,9 +630,9 @@ suite('FileSearchEngine', () => { let engine = new FileSearchEngine({ folderQueries: [], extraFiles: [ - path.normalize(path.join(require.toUrl('./fixtures'), 'site.css')), - path.normalize(path.join(require.toUrl('./fixtures'), 'examples', 'company.js')), - path.normalize(path.join(require.toUrl('./fixtures'), 'index.html')) + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'site.css')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'examples', 'company.js')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'index.html')) ], filePattern: '*.js' }); @@ -702,9 +657,9 @@ suite('FileSearchEngine', () => { let engine = new FileSearchEngine({ folderQueries: [], extraFiles: [ - path.normalize(path.join(require.toUrl('./fixtures'), 'site.css')), - path.normalize(path.join(require.toUrl('./fixtures'), 'examples', 'company.js')), - path.normalize(path.join(require.toUrl('./fixtures'), 'index.html')) + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'site.css')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'examples', 'company.js')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'index.html')) ], filePattern: '*.*', includePattern: { '**/*.css': true } @@ -730,9 +685,9 @@ suite('FileSearchEngine', () => { let engine = new FileSearchEngine({ folderQueries: [], extraFiles: [ - path.normalize(path.join(require.toUrl('./fixtures'), 'site.css')), - path.normalize(path.join(require.toUrl('./fixtures'), 'examples', 'company.js')), - path.normalize(path.join(require.toUrl('./fixtures'), 'index.html')) + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'site.css')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'examples', 'company.js')), + path.normalize(path.join(getPathFromAmdModule(require, './fixtures'), 'index.html')) ], filePattern: '*.*', excludePattern: { '**/*.css': true } @@ -988,4 +943,4 @@ suite('FileWalker', () => { const lines = stdout.split('\n'); return files.every(file => lines.indexOf(file) >= 0); } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts index 5d33088e881..37264624d97 100644 --- a/src/vs/workbench/services/search/test/node/searchService.test.ts +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -7,29 +7,28 @@ import * as assert from 'assert'; import * as path from 'path'; - -import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search'; -import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete, IFolderSearch } from 'vs/workbench/services/search/node/search'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IProgress, ISearchEngineStats, IFileSearchStats } from 'vs/platform/search/common/search'; import { SearchService as RawSearchService } from 'vs/workbench/services/search/node/rawSearchService'; +import { IFolderSearch, IRawFileMatch, IRawSearch, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, ISearchEngineSuccess } from 'vs/workbench/services/search/node/search'; import { DiskSearch } from 'vs/workbench/services/search/node/searchService'; const TEST_FOLDER_QUERIES = [ { folder: path.normalize('/some/where') } ]; -const TEST_FIXTURES = path.normalize(require.toUrl('./fixtures')); +const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures')); const MULTIROOT_QUERIES: IFolderSearch[] = [ { folder: path.join(TEST_FIXTURES, 'examples') }, { folder: path.join(TEST_FIXTURES, 'more') } ]; -const stats: IUncachedSearchStats = { - fromCache: false, - resultCount: 4, +const stats: ISearchEngineStats = { traversal: 'node', - errors: [], - fileWalkStartTime: 0, - fileWalkResultTime: 1, + fileWalkTime: 0, + cmdTime: 1, directoriesWalked: 2, filesWalked: 3 }; @@ -44,7 +43,7 @@ class TestSearchEngine implements ISearchEngine { TestSearchEngine.last = this; } - public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void { + public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISearchEngineSuccess) => void): void { const self = this; (function next() { process.nextTick(() => { @@ -101,17 +100,17 @@ suite('SearchService', () => { const service = new RawSearchService(); let results = 0; - return service.doFileSearch(Engine, rawSearch) - .then(() => { - assert.strictEqual(results, 5); - }, null, value => { - if (!Array.isArray(value)) { - assert.deepStrictEqual(value, match); - results++; - } else { - assert.fail(JSON.stringify(value)); - } - }); + const cb: (p: ISerializedSearchProgressItem) => void = value => { + if (!Array.isArray(value)) { + assert.deepStrictEqual(value, match); + results++; + } else { + assert.fail(JSON.stringify(value)); + } + }; + + return service.doFileSearch(Engine, rawSearch, cb) + .then(() => assert.strictEqual(results, 5)); }); test('Batch results', function () { @@ -121,19 +120,20 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, rawSearch, 10) - .then(() => { - assert.deepStrictEqual(results, [10, 10, 5]); - }, null, value => { - if (Array.isArray(value)) { - value.forEach(m => { - assert.deepStrictEqual(m, match); - }); - results.push(value.length); - } else { - assert.fail(JSON.stringify(value)); - } - }); + const cb: (p: ISerializedSearchProgressItem) => void = value => { + if (Array.isArray(value)) { + value.forEach(m => { + assert.deepStrictEqual(m, match); + }); + results.push(value.length); + } else { + assert.fail(JSON.stringify(value)); + } + }; + + return service.doFileSearch(Engine, rawSearch, cb, undefined, 10).then(() => { + assert.deepStrictEqual(results, [10, 10, 5]); + }); }); test('Collect batched results', function () { @@ -143,14 +143,32 @@ suite('SearchService', () => { const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch); const service = new RawSearchService(); + function fileSearch(config: IRawSearch, batchSize: number): Event { + let promise: CancelablePromise; + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + promise = createCancelablePromise(token => service.doFileSearch(Engine, config, p => emitter.fire(p), token, batchSize) + .then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err }))); + }, + onLastListenerRemove: () => { + promise.cancel(); + } + }); + + return emitter.event; + } + const progressResults = []; - return DiskSearch.collectResults(service.doFileSearch(Engine, rawSearch, 10)) + const onProgress = match => { + assert.strictEqual(match.resource.path, uriPath); + progressResults.push(match); + }; + + return DiskSearch.collectResultsFromEvent(fileSearch(rawSearch, 10), onProgress) .then(result => { assert.strictEqual(result.results.length, 25, 'Result'); assert.strictEqual(progressResults.length, 25, 'Progress'); - }, null, match => { - assert.strictEqual(match.resource.path, uriPath); - progressResults.push(match); }); }); @@ -167,7 +185,7 @@ suite('SearchService', () => { }, }; - return DiskSearch.collectResults(service.fileSearch(query)) + return DiskSearch.collectResultsFromEvent(service.fileSearch(query)) .then(result => { assert.strictEqual(result.results.length, 1, 'Result'); }); @@ -186,7 +204,7 @@ suite('SearchService', () => { }, }; - return DiskSearch.collectResults(service.fileSearch(query)) + return DiskSearch.collectResultsFromEvent(service.fileSearch(query)) .then(result => { assert.strictEqual(result.results.length, 0, 'Result'); assert.ok(result.limitHit); @@ -206,20 +224,22 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bb', - sortByScore: true, - maxResults: 2 - }, 1).then(() => { - assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number'); - assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bb', + sortByScore: true, + maxResults: 2 + }, cb, undefined, 1).then(() => { + assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number'); + assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]); }); }); @@ -230,23 +250,24 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; + const cb = value => { + if (Array.isArray(value)) { + value.forEach(m => { + assert.deepStrictEqual(m, match); + }); + results.push(value.length); + } else { + assert.fail(JSON.stringify(value)); + } + }; return service.doFileSearch(Engine, { folderQueries: TEST_FOLDER_QUERIES, filePattern: 'a', sortByScore: true, maxResults: 23 - }, 10) + }, cb, undefined, 10) .then(() => { assert.deepStrictEqual(results, [10, 10, 3]); - }, null, value => { - if (Array.isArray(value)) { - value.forEach(m => { - assert.deepStrictEqual(m, match); - }); - results.push(value.length); - } else { - assert.fail(JSON.stringify(value)); - } }); }); @@ -263,37 +284,39 @@ suite('SearchService', () => { const service = new RawSearchService(); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'b', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'b', + sortByScore: true, + cacheKey: 'x' + }, cb, undefined, -1).then(complete => { + assert.strictEqual((complete.stats).fromCache, false); + assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]); }).then(() => { const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bc', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.ok(complete.stats.fromCache); - assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } - }); + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bc', + sortByScore: true, + cacheKey: 'x' + }, cb, undefined, -1).then(complete => { + assert.ok((complete.stats).fromCache); + assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]); + }, null); }).then(() => { return service.clearCache('x'); }).then(() => { @@ -304,21 +327,22 @@ suite('SearchService', () => { size: 3 }); const results = []; - return service.doFileSearch(Engine, { - folderQueries: TEST_FOLDER_QUERIES, - filePattern: 'bc', - sortByScore: true, - cacheKey: 'x' - }, -1).then(complete => { - assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]); - }, null, value => { + const cb = value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); } else { assert.fail(JSON.stringify(value)); } + }; + return service.doFileSearch(Engine, { + folderQueries: TEST_FOLDER_QUERIES, + filePattern: 'bc', + sortByScore: true, + cacheKey: 'x' + }, cb, undefined, -1).then(complete => { + assert.strictEqual((complete.stats).fromCache, false); + assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]); }); }); }); -}); \ No newline at end of file +}); 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 93a86a86b12..21484acb566 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -15,12 +15,13 @@ import { ISerializedFileMatch, IRawSearch, IFolderSearch } from 'vs/workbench/se import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch'; import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch'; import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; function countAll(matches: ISerializedFileMatch[]): number { return matches.reduce((acc, m) => acc + m.numMatches, 0); } -const TEST_FIXTURES = path.normalize(require.toUrl('./fixtures')); +const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures')); const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples'); const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more'); const TEST_ROOT_FOLDER: IFolderSearch = { folder: TEST_FIXTURES }; diff --git a/src/vs/workbench/services/textMate/electron-browser/TMSyntax.ts b/src/vs/workbench/services/textMate/electron-browser/TMSyntax.ts index 59013467db6..0294ecefead 100644 --- a/src/vs/workbench/services/textMate/electron-browser/TMSyntax.ts +++ b/src/vs/workbench/services/textMate/electron-browser/TMSyntax.ts @@ -7,8 +7,8 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; +import * as resources from 'vs/base/common/resources'; import { Event, Emitter } from 'vs/base/common/event'; -import { join, normalize } from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -22,7 +22,10 @@ import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/t import { nullTokenize2 } from 'vs/editor/common/modes/nullMode'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; import { Color } from 'vs/base/common/color'; -import URI from 'vs/base/common/uri'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; export class TMScopeRegistry { @@ -37,27 +40,27 @@ export class TMScopeRegistry { this._encounteredLanguages = []; } - public register(scopeName: string, filePath: string, embeddedLanguages?: IEmbeddedLanguagesMap, tokenTypes?: TokenTypesContribution): void { + public register(scopeName: string, grammarLocation: URI, embeddedLanguages?: IEmbeddedLanguagesMap, tokenTypes?: TokenTypesContribution): void { if (this._scopeNameToLanguageRegistration[scopeName]) { const existingRegistration = this._scopeNameToLanguageRegistration[scopeName]; - if (existingRegistration.grammarFilePath !== filePath) { + if (!resources.isEqual(existingRegistration.grammarLocation, grammarLocation)) { console.warn( `Overwriting grammar scope name to file mapping for scope ${scopeName}.\n` + - `Old grammar file: ${existingRegistration.grammarFilePath}.\n` + - `New grammar file: ${filePath}` + `Old grammar file: ${existingRegistration.grammarLocation.toString()}.\n` + + `New grammar file: ${grammarLocation.toString()}` ); } } - this._scopeNameToLanguageRegistration[scopeName] = new TMLanguageRegistration(scopeName, filePath, embeddedLanguages, tokenTypes); + this._scopeNameToLanguageRegistration[scopeName] = new TMLanguageRegistration(scopeName, grammarLocation, embeddedLanguages, tokenTypes); } public getLanguageRegistration(scopeName: string): TMLanguageRegistration { return this._scopeNameToLanguageRegistration[scopeName] || null; } - public getFilePath(scopeName: string): string { + public getGrammarLocation(scopeName: string): URI { let data = this.getLanguageRegistration(scopeName); - return data ? data.grammarFilePath : null; + return data ? data.grammarLocation : null; } /** @@ -75,13 +78,13 @@ export class TMLanguageRegistration { _topLevelScopeNameDataBrand: void; readonly scopeName: string; - readonly grammarFilePath: string; + readonly grammarLocation: URI; readonly embeddedLanguages: IEmbeddedLanguagesMap; readonly tokenTypes: ITokenTypeMap; - constructor(scopeName: string, grammarFilePath: string, embeddedLanguages: IEmbeddedLanguagesMap, tokenTypes: TokenTypesContribution | undefined) { + constructor(scopeName: string, grammarLocation: URI, embeddedLanguages: IEmbeddedLanguagesMap, tokenTypes: TokenTypesContribution | undefined) { this.scopeName = scopeName; - this.grammarFilePath = grammarFilePath; + this.grammarLocation = grammarLocation; // embeddedLanguages handling this.embeddedLanguages = Object.create(null); @@ -136,9 +139,12 @@ export class TextMateService implements ITextMateService { private _grammarRegistry: TPromise<[Registry, StackElement]>; private _modeService: IModeService; private _themeService: IWorkbenchThemeService; + private _fileService: IFileService; + private _logService: ILogService; private _scopeRegistry: TMScopeRegistry; private _injections: { [scopeName: string]: string[]; }; private _injectedEmbeddedLanguages: { [scopeName: string]: IEmbeddedLanguagesMap[]; }; + private _notificationService: INotificationService; private _languageToScope: Map; private _styleElement: HTMLStyleElement; @@ -149,17 +155,23 @@ export class TextMateService implements ITextMateService { constructor( @IModeService modeService: IModeService, - @IWorkbenchThemeService themeService: IWorkbenchThemeService + @IWorkbenchThemeService themeService: IWorkbenchThemeService, + @IFileService fileService: IFileService, + @INotificationService notificationService: INotificationService, + @ILogService logService: ILogService ) { this._styleElement = dom.createStyleSheet(); this._styleElement.className = 'vscode-tokens-styles'; this._modeService = modeService; this._themeService = themeService; + this._fileService = fileService; + this._logService = logService; this._scopeRegistry = new TMScopeRegistry(); this.onDidEncounterLanguage = this._scopeRegistry.onDidEncounterLanguage; this._injections = {}; this._injectedEmbeddedLanguages = {}; this._languageToScope = new Map(); + this._notificationService = notificationService; this._grammarRegistry = null; @@ -199,10 +211,20 @@ export class TextMateService implements ITextMateService { private _getOrCreateGrammarRegistry(): TPromise<[Registry, StackElement]> { if (!this._grammarRegistry) { - this._grammarRegistry = TPromise.wrap(import('vscode-textmate')).then(({ Registry, INITIAL }) => { + this._grammarRegistry = TPromise.wrap(import('vscode-textmate')).then(({ Registry, INITIAL, parseRawGrammar }) => { const grammarRegistry = new Registry({ - getFilePath: (scopeName: string) => { - return this._scopeRegistry.getFilePath(scopeName); + loadGrammar: (scopeName: string) => { + const location = this._scopeRegistry.getGrammarLocation(scopeName); + if (!location) { + this._logService.trace(`No grammar found for scope ${scopeName}`); + return null; + } + return this._fileService.resolveContent(location, { encoding: 'utf8' }).then(content => { + return parseRawGrammar(content.value, location.path); + }, e => { + this._logService.error(`Unable to load and parse grammar for scope ${scopeName} from ${location}`, e); + return null; + }); }, getInjections: (scopeName: string) => { return this._injections[scopeName]; @@ -290,14 +312,12 @@ export class TextMateService implements ITextMateService { return; } - //TODO@extensionLocation - let normalizedAbsolutePath = normalize(join(extensionLocation.fsPath, syntax.path)); - - if (normalizedAbsolutePath.indexOf(extensionLocation.fsPath) !== 0) { - collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", grammarsExtPoint.name, normalizedAbsolutePath, extensionLocation.fsPath)); + const grammarLocation = resources.joinPath(extensionLocation, syntax.path); + if (!resources.isEqualOrParent(grammarLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", grammarsExtPoint.name, grammarLocation.path, extensionLocation.path)); } - this._scopeRegistry.register(syntax.scopeName, normalizedAbsolutePath, syntax.embeddedLanguages, syntax.tokenTypes); + this._scopeRegistry.register(syntax.scopeName, grammarLocation, syntax.embeddedLanguages, syntax.tokenTypes); if (syntax.injectTo) { for (let injectScope of syntax.injectTo) { @@ -365,25 +385,20 @@ export class TextMateService implements ITextMateService { let containsEmbeddedLanguages = (Object.keys(embeddedLanguages).length > 0); return this._getOrCreateGrammarRegistry().then((_res) => { const [grammarRegistry, initialState] = _res; - return new TPromise((c, e, p) => { - grammarRegistry.loadGrammarWithConfiguration(scopeName, languageId, { embeddedLanguages, tokenTypes: languageRegistration.tokenTypes }, (err, grammar) => { - if (err) { - return e(err); - } - c({ - languageId: languageId, - grammar: grammar, - initialState: initialState, - containsEmbeddedLanguages: containsEmbeddedLanguages - }); - }); + return grammarRegistry.loadGrammarWithConfiguration(scopeName, languageId, { embeddedLanguages, tokenTypes: languageRegistration.tokenTypes }).then(grammar => { + return { + languageId: languageId, + grammar: grammar, + initialState: initialState, + containsEmbeddedLanguages: containsEmbeddedLanguages + }; }); }); } private registerDefinition(modeId: string): void { this._createGrammar(modeId).then((r) => { - TokenizationRegistry.register(modeId, new TMTokenization(this._scopeRegistry, r.languageId, r.grammar, r.initialState, r.containsEmbeddedLanguages)); + TokenizationRegistry.register(modeId, new TMTokenization(this._scopeRegistry, r.languageId, r.grammar, r.initialState, r.containsEmbeddedLanguages, this._notificationService)); }, onUnexpectedError); } } @@ -396,8 +411,9 @@ class TMTokenization implements ITokenizationSupport { private readonly _containsEmbeddedLanguages: boolean; private readonly _seenLanguages: boolean[]; private readonly _initialState: StackElement; + private _tokenizationWarningAlreadyShown: boolean; - constructor(scopeRegistry: TMScopeRegistry, languageId: LanguageId, grammar: IGrammar, initialState: StackElement, containsEmbeddedLanguages: boolean) { + constructor(scopeRegistry: TMScopeRegistry, languageId: LanguageId, grammar: IGrammar, initialState: StackElement, containsEmbeddedLanguages: boolean, @INotificationService private notificationService: INotificationService) { this._scopeRegistry = scopeRegistry; this._languageId = languageId; this._grammar = grammar; @@ -421,6 +437,10 @@ class TMTokenization implements ITokenizationSupport { // Do not attempt to tokenize if a line has over 20k if (line.length >= 20000) { + if (!this._tokenizationWarningAlreadyShown) { + this._tokenizationWarningAlreadyShown = true; + this.notificationService.warn(nls.localize('too many characters', "Tokenization is skipped for lines longer than 20k characters for performance reasons.")); + } console.log(`Line (${line.substr(0, 15)}...): longer than 20k characters, tokenization skipped.`); return nullTokenize2(this._languageId, line, state, offsetDelta); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index a625dbdba08..5e727d770c4 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -8,17 +8,14 @@ import * as path from 'vs/base/common/paths'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { TPromise, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { guessMimeTypes } from 'vs/base/common/mime'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import URI from 'vs/base/common/uri'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as diagnostics from 'vs/base/common/diagnostics'; -import * as types from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { isUndefinedOrNull } from 'vs/base/common/types'; import { IMode } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -27,23 +24,37 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, timeout } from 'vs/base/common/async'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isLinux } from 'vs/base/common/platform'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; /** * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk. */ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel { - public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; - public static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100; + static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; + static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100; + static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json']; + static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json']; private static saveErrorHandler: ISaveErrorHandler; + static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; } + private static saveParticipant: ISaveParticipant; + static setSaveParticipant(handler: ISaveParticipant): void { TextFileEditorModel.saveParticipant = handler; } + + private readonly _onDidContentChange: Emitter = this._register(new Emitter()); + get onDidContentChange(): Event { return this._onDidContentChange.event; } + + private readonly _onDidStateChange: Emitter = this._register(new Emitter()); + get onDidStateChange(): Event { return this._onDidStateChange.event; } private resource: URI; private contentEncoding: string; // encoding as reported from disk @@ -52,20 +63,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private versionId: number; private bufferSavedVersionId: number; private lastResolvedDiskStat: IFileStat; - private toDispose: IDisposable[]; private blockModelContentChange: boolean; private autoSaveAfterMillies: number; private autoSaveAfterMilliesEnabled: boolean; - private autoSavePromise: TPromise; + private autoSaveDisposable: IDisposable; private contentChangeEventScheduler: RunOnceScheduler; private orphanedChangeEventScheduler: RunOnceScheduler; private saveSequentializer: SaveSequentializer; private disposed: boolean; private lastSaveAttemptTime: number; private createTextEditorModelPromise: TPromise; - private readonly _onDidContentChange: Emitter; - private readonly _onDidStateChange: Emitter; - private inConflictMode: boolean; private inOrphanMode: boolean; private inErrorMode: boolean; @@ -83,15 +90,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IBackupFileService private backupFileService: IBackupFileService, @IEnvironmentService private environmentService: IEnvironmentService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IHashService private hashService: IHashService + @IHashService private hashService: IHashService, + @ILogService private logService: ILogService ) { super(modelService, modeService); + this.resource = resource; - this.toDispose = []; - this._onDidContentChange = new Emitter(); - this._onDidStateChange = new Emitter(); - this.toDispose.push(this._onDidContentChange); - this.toDispose.push(this._onDidStateChange); this.preferredEncoding = preferredEncoding; this.inOrphanMode = false; this.dirty = false; @@ -99,11 +103,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.lastSaveAttemptTime = 0; this.saveSequentializer = new SaveSequentializer(); - this.contentChangeEventScheduler = new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY); - this.toDispose.push(this.contentChangeEventScheduler); - - this.orphanedChangeEventScheduler = new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY); - this.toDispose.push(this.orphanedChangeEventScheduler); + this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY)); + this.orphanedChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY)); this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration()); @@ -111,10 +112,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private registerListeners(): void { - this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this.toDispose.push(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config))); - this.toDispose.push(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange())); - this.toDispose.push(this.onDidStateChange(e => this.onStateChange(e))); + this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config))); + this._register(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange())); + this._register(this.onDidStateChange(e => this.onStateChange(e))); } private onStateChange(e: StateChange): void { @@ -151,13 +152,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) { - let checkOrphanedPromise: TPromise; + let checkOrphanedPromise: Thenable; if (newInOrphanModeGuess) { // We have received reports of users seeing delete events even though the file still // exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665). // Since we do not want to mark the model as orphaned, we have to check if the // file is really gone and not just a faulty file event. - checkOrphanedPromise = TPromise.timeout(100).then(() => { + checkOrphanedPromise = timeout(100).then(() => { if (this.disposed) { return true; } @@ -165,10 +166,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.fileService.existsFile(this.resource).then(exists => !exists); }); } else { - checkOrphanedPromise = TPromise.as(false); + checkOrphanedPromise = Promise.resolve(false); } - checkOrphanedPromise.done(newInOrphanModeValidated => { + checkOrphanedPromise.then(newInOrphanModeValidated => { if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) { this.setOrphaned(newInOrphanModeValidated); } @@ -184,71 +185,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void { - if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) { - this.autoSaveAfterMillies = config.autoSaveDelay; - this.autoSaveAfterMilliesEnabled = true; - } else { - this.autoSaveAfterMillies = void 0; - this.autoSaveAfterMilliesEnabled = false; - } + const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0; + + this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled; + this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : void 0; } private onFilesAssociationChange(): void { - this.updateTextEditorModelMode(); - } - - private updateTextEditorModelMode(modeId?: string): void { if (!this.textEditorModel) { return; } const firstLineText = this.getFirstLineText(this.textEditorModel); - const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText); + const mode = this.getOrCreateMode(this.modeService, void 0, firstLineText); this.modelService.setMode(this.textEditorModel, mode); } - public get onDidContentChange(): Event { - return this._onDidContentChange.event; - } - - public get onDidStateChange(): Event { - return this._onDidStateChange.event; - } - - /** - * The current version id of the model. - */ - public getVersionId(): number { + getVersionId(): number { return this.versionId; } - /** - * Set a save error handler to install code that executes when save errors occur. - */ - public static setSaveErrorHandler(handler: ISaveErrorHandler): void { - TextFileEditorModel.saveErrorHandler = handler; - } - - /** - * Set a save participant handler to react on models getting saved. - */ - public static setSaveParticipant(handler: ISaveParticipant): void { - TextFileEditorModel.saveParticipant = handler; - } - - /** - * Discards any local changes and replaces the model with the contents of the version on disk. - * - * @param if the parameter soft is true, will not attempt to load the contents from disk. - */ - public revert(soft?: boolean): TPromise { + revert(soft?: boolean): TPromise { if (!this.isResolved()) { return TPromise.wrap(null); } // Cancel any running auto-save - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); // Unset flags const undo = this.setDirty(false); @@ -273,28 +237,28 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }); } - public load(options?: ILoadOptions): TPromise { - diag('load() - enter', this.resource, new Date()); + load(options?: ILoadOptions): TPromise { + this.logService.trace('load() - enter', this.resource); - // It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk - // if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk - // meanwhile, but this is a very low risk. + // It is very important to not reload the model when the model is dirty. + // We also only want to reload the model from the disk if no save is pending + // to avoid data loss. if (this.dirty || this.saveSequentializer.hasPendingSave()) { - diag('load() - exit - without loading because model is dirty or being saved', this.resource, new Date()); + this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource); return TPromise.as(this); } // Only for new models we support to load from backup if (!this.textEditorModel && !this.createTextEditorModelPromise) { - return this.loadWithBackup(options); + return this.loadFromBackup(options); } // Otherwise load from file resource return this.loadFromFile(options); } - private loadWithBackup(options?: ILoadOptions): TPromise { + private loadFromBackup(options?: ILoadOptions): TPromise { return this.backupFileService.loadBackupResource(this.resource).then(backup => { // Make sure meanwhile someone else did not suceed or start loading @@ -310,10 +274,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil mtime: Date.now(), etag: void 0, value: createTextBufferFactory(''), /* will be filled later from backup */ - encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding) + encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding), + isReadonly: false }; - return this.loadWithContent(content, backup); + return this.loadWithContent(content, options, backup); } // Otherwise load from file @@ -333,62 +298,77 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching } + // Ensure to track the versionId before doing a long running operation + // to make sure the model was not changed in the meantime which would + // indicate that the user or program has made edits. If we would ignore + // this, we could potentially loose the changes that were made because + // after resolving the content we update the model and reset the dirty + // flag. + const currentVersionId = this.versionId; + // Resolve Content return this.textFileService .resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }) - .then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error)); + .then(content => { + + // Clear orphaned state when loading was successful + this.setOrphaned(false); + + // Guard against the model having changed in the meantime + if (currentVersionId === this.versionId) { + return this.loadWithContent(content, options); + } + + return this; + }, error => { + const result = error.fileOperationResult; + + // Apply orphaned state based on error code + this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); + + // NotModified status is expected and can be handled gracefully + if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + + // Guard against the model having changed in the meantime + if (currentVersionId === this.versionId) { + this.setDirty(false); // Ensure we are not tracking a stale state + } + + return TPromise.as(this); + } + + // Ignore when a model has been resolved once and the file was deleted meanwhile. Since + // we already have the model loaded, we can return to this state and update the orphaned + // flag to indicate that this model has no version on disk anymore. + if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { + return TPromise.as(this); + } + + // Otherwise bubble up the error + return TPromise.wrapError(error); + }); } - private handleLoadSuccess(content: IRawTextContent): TPromise { - - // Clear orphaned state when load was successful - this.setOrphaned(false); - - return this.loadWithContent(content); - } - - private handleLoadError(error: FileOperationError): TPromise { - const result = error.fileOperationResult; - - // Apply orphaned state based on error code - this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); - - // NotModified status is expected and can be handled gracefully - if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - this.setDirty(false); // Ensure we are not tracking a stale state - - return TPromise.as(this); - } - - // Ignore when a model has been resolved once and the file was deleted meanwhile. Since - // we already have the model loaded, we can return to this state and update the orphaned - // flag to indicate that this model has no version on disk anymore. - if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { - return TPromise.as(this); - } - - // Otherwise bubble up the error - return TPromise.wrapError(error); - } - - private loadWithContent(content: IRawTextContent, backup?: URI): TPromise { + private loadWithContent(content: IRawTextContent, options?: ILoadOptions, backup?: URI): TPromise { return this.doLoadWithContent(content, backup).then(model => { - // Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype - if (this.isSettingsFile()) { + const settingsType = this.getTypeIfSettings(); + if (settingsType) { /* __GDPR__ - "settingsRead" : {} + "settingsRead" : { + "settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } */ - this.telemetryService.publicLog('settingsRead'); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data + this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data } else { /* __GDPR__ "fileGet" : { - "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "${include}": [ + "${FileTelemetryData}" + ] } */ - this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.resource.fsPath), path: this.hashService.createSHA1(this.resource.fsPath) }); + this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER)); } return model; @@ -396,19 +376,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doLoadWithContent(content: IRawTextContent, backup?: URI): TPromise { - diag('load() - resolved content', this.resource, new Date()); + this.logService.trace('load() - resolved content', this.resource); // Update our resolved disk stat model - const resolvedStat: IFileStat = { + this.updateLastResolvedDiskStat({ resource: this.resource, name: content.name, mtime: content.mtime, etag: content.etag, isDirectory: false, isSymbolicLink: false, - children: void 0 - }; - this.updateLastResolvedDiskStat(resolvedStat); + children: void 0, + isReadonly: content.isReadonly + } as IFileStat); // Keep the original encoding to not loose it when saving const oldEncoding = this.contentEncoding; @@ -428,7 +408,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Join an existing request to create the editor model to avoid race conditions else if (this.createTextEditorModelPromise) { - diag('load() - join existing text editor model promise', this.resource, new Date()); + this.logService.trace('load() - join existing text editor model promise', this.resource); return this.createTextEditorModelPromise; } @@ -438,7 +418,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): TPromise { - diag('load() - updated text editor model', this.resource, new Date()); + this.logService.trace('load() - updated text editor model', this.resource); // Ensure we are not tracking a stale state this.setDirty(false); @@ -458,7 +438,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI): TPromise { - diag('load() - created text editor model', this.resource, new Date()); + this.logService.trace('load() - created text editor model', this.resource); this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => { const hasBackupContent = !!backupContent; @@ -502,7 +482,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // where `value` was captured in the content change listener closure scope. // Content Change - this.toDispose.push(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); + this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); } private doLoadBackup(backup: URI): TPromise { @@ -518,11 +498,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private onModelContentChanged(): void { - diag(`onModelContentChanged() - enter`, this.resource, new Date()); + this.logService.trace(`onModelContentChanged() - enter`, this.resource); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date()); + this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource); // Ignore if blocking model changes if (this.blockModelContentChange) { @@ -534,7 +514,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Note: we currently only do this check when auto-save is turned off because there you see // a dirty indicator that you want to get rid of when undoing to the saved version. if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) { - diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date()); + this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource); // Clear flags const wasDirty = this.dirty; @@ -548,7 +528,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - diag('onModelContentChanged() - model content changed and marked as dirty', this.resource, new Date()); + this.logService.trace('onModelContentChanged() - model content changed and marked as dirty', this.resource); // Mark as dirty this.makeDirty(); @@ -558,7 +538,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (!this.inConflictMode) { this.doAutoSave(this.versionId); } else { - diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date()); + this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource); } } @@ -578,53 +558,50 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private doAutoSave(versionId: number): TPromise { - diag(`doAutoSave() - enter for versionId ${versionId}`, this.resource, new Date()); + private doAutoSave(versionId: number): void { + this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource); // Cancel any currently running auto saves to make this the one that succeeds - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); - // Create new save promise and keep it - this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => { + // Create new save timer and store it for disposal as needed + const handle = setTimeout(() => { // Only trigger save if the version id has not changed meanwhile if (versionId === this.versionId) { - this.doSave(versionId, { reason: SaveReason.AUTO }).done(null, onUnexpectedError); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change + this.doSave(versionId, { reason: SaveReason.AUTO }); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change } - }); + }, this.autoSaveAfterMillies); - return this.autoSavePromise; + this.autoSaveDisposable = toDisposable(() => clearTimeout(handle)); } - private cancelAutoSavePromise(): void { - if (this.autoSavePromise) { - this.autoSavePromise.cancel(); - this.autoSavePromise = void 0; + private cancelPendingAutoSave(): void { + if (this.autoSaveDisposable) { + this.autoSaveDisposable.dispose(); + this.autoSaveDisposable = void 0; } } - /** - * Saves the current versionId of this editor model if it is dirty. - */ - public save(options: ISaveOptions = Object.create(null)): TPromise { + save(options: ISaveOptions = Object.create(null)): TPromise { if (!this.isResolved()) { return TPromise.wrap(null); } - diag('save() - enter', this.resource, new Date()); + this.logService.trace('save() - enter', this.resource); // Cancel any currently running auto saves to make this the one that succeeds - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); return this.doSave(this.versionId, options); } private doSave(versionId: number, options: ISaveOptions): TPromise { - if (types.isUndefinedOrNull(options.reason)) { + if (isUndefinedOrNull(options.reason)) { options.reason = SaveReason.EXPLICIT; } - diag(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource); // Lookup any running pending save for this versionId and return it if found // @@ -632,7 +609,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPendingSave(versionId)) { - diag(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource); return this.saveSequentializer.pendingSave; } @@ -645,7 +622,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Thus we avoid spawning multiple auto saves and only take the latest. // if ((!options.force && !this.dirty) || versionId !== this.versionId) { - diag(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource); return TPromise.wrap(null); } @@ -659,7 +636,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPendingSave()) { - diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource); // Register this as the next upcoming save and return return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options)); @@ -698,7 +675,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered // one after the other without waiting for the save() to complete. If we are disposed(), we risk // saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942). - // To fix this issue, we will not store the contents to disk when we got disposed. + // To fix this issue, we will not store the contents to disk when we got disposed. if (this.disposed) { return void 0; } @@ -725,7 +702,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) - diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource); return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, @@ -734,30 +711,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil etag: this.lastResolvedDiskStat.etag, writeElevated: options.writeElevated }).then(stat => { - diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource); // Telemetry - if (this.isSettingsFile()) { + const settingsType = this.getTypeIfSettings(); + if (settingsType) { /* __GDPR__ - "settingsWritten" : {} + "settingsWritten" : { + "settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } */ - this.telemetryService.publicLog('settingsWritten'); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data + this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data } else { /* __GDPR__ "filePUT" : { - "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "${include}": [ + "${FileTelemetryData}" + ] } */ - this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: path.extname(this.lastResolvedDiskStat.resource.fsPath) }); + this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason)); } // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - diag(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource); this.setDirty(false); } else { - diag(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource, new Date()); + this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource); } // Updated resolved stat with updated stat @@ -769,7 +750,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit File Saved Event this._onDidStateChange.fire(StateChange.SAVED); }, error => { - diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date()); + if (!error) { + error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051) + } + + this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource); // Flag as error state in the model this.inErrorMode = true; @@ -788,17 +773,69 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil })); } - private isSettingsFile(): boolean { + private getTypeIfSettings(): string { + if (path.extname(this.resource.fsPath) !== '.json') { + return ''; + } // Check for global settings file - if (path.isEqual(this.resource.fsPath, this.environmentService.appSettingsPath, !isLinux)) { - return true; + if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) { + return 'global-settings'; + } + + // Check for keybindings file + if (isEqual(this.resource, URI.file(this.environmentService.appKeybindingsPath), !isLinux)) { + return 'keybindings'; + } + + // Check for locale file + if (isEqual(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) { + return 'locale'; + } + + // Check for snippets + if (isEqualOrParent(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'snippets')))) { + return 'snippets'; } // Check for workspace settings file - return this.contextService.getWorkspace().folders.some(folder => { - return path.isEqualOrParent(this.resource.fsPath, path.join(folder.uri.fsPath, '.vscode')); - }); + const folders = this.contextService.getWorkspace().folders; + for (let i = 0; i < folders.length; i++) { + if (isEqualOrParent(this.resource, folders[i].toResource('.vscode'))) { + const filename = path.basename(this.resource.fsPath); + if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) { + return `.vscode/${filename}`; + } + } + } + + return ''; + } + + private getTelemetryData(reason: number): Object { + const ext = path.extname(this.resource.fsPath); + const fileName = path.basename(this.resource.fsPath); + const telemetryData = { + mimeType: guessMimeTypes(this.resource.fsPath).join(', '), + ext, + path: this.hashService.createSHA1(this.resource.fsPath), + reason + }; + + if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) { + telemetryData['whitelistedjson'] = fileName; + } + + /* __GDPR__FRAGMENT__ + "FileTelemetryData" : { + "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + return telemetryData; } private doTouch(versionId: number): TPromise { @@ -874,31 +911,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil TextFileEditorModel.saveErrorHandler.onSaveError(error, this); } - /** - * Returns true if the content of this model has changes that are not yet saved back to the disk. - */ - public isDirty(): boolean { + isDirty(): boolean { return this.dirty; } - /** - * Returns the time in millies when this working copy was attempted to be saved. - */ - public getLastSaveAttemptTime(): number { + getLastSaveAttemptTime(): number { return this.lastSaveAttemptTime; } - /** - * Returns the time in millies when this working copy was last modified by the user or some other program. - */ - public getETag(): string { + getETag(): string { return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null; } - /** - * Answers if this model is in a specific state. - */ - public hasState(state: ModelState): boolean { + hasState(state: ModelState): boolean { switch (state) { case ModelState.CONFLICT: return this.inConflictMode; @@ -915,11 +940,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - public getEncoding(): string { + getEncoding(): string { return this.preferredEncoding || this.contentEncoding; } - public setEncoding(encoding: string, mode: EncodingMode): void { + setEncoding(encoding: string, mode: EncodingMode): void { if (!this.isNewEncoding(encoding)) { return; // return early if the encoding is already the same } @@ -935,7 +960,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (!this.inConflictMode) { - this.save({ overwriteEncoding: true }).done(null, onUnexpectedError); + this.save({ overwriteEncoding: true }); } } @@ -952,11 +977,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Load this.load({ forceReadFromDisk: true // because encoding has changed - }).done(null, onUnexpectedError); + }); } } - public updatePreferredEncoding(encoding: string): void { + updatePreferredEncoding(encoding: string): void { if (!this.isNewEncoding(encoding)) { return; } @@ -979,41 +1004,35 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return true; } - public isResolved(): boolean { - return !types.isUndefinedOrNull(this.lastResolvedDiskStat); + isResolved(): boolean { + return !isUndefinedOrNull(this.lastResolvedDiskStat); } - /** - * Returns true if the dispose() method of this model has been called. - */ - public isDisposed(): boolean { + isReadonly(): boolean { + return this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly; + } + + isDisposed(): boolean { return this.disposed; } - /** - * Returns the full resource URI of the file this text file editor model is about. - */ - public getResource(): URI { + getResource(): URI { return this.resource; } - /** - * Stat accessor only used by tests. - */ - public getStat(): IFileStat { + getStat(): IFileStat { return this.lastResolvedDiskStat; } - public dispose(): void { + dispose(): void { this.disposed = true; this.inConflictMode = false; this.inOrphanMode = false; this.inErrorMode = false; - this.toDispose = dispose(this.toDispose); this.createTextEditorModelPromise = null; - this.cancelAutoSavePromise(); + this.cancelPendingAutoSave(); super.dispose(); } @@ -1035,7 +1054,7 @@ export class SaveSequentializer { private _pendingSave: IPendingSave; private _nextSave: ISaveOperation; - public hasPendingSave(versionId?: number): boolean { + hasPendingSave(versionId?: number): boolean { if (!this._pendingSave) { return false; } @@ -1047,14 +1066,14 @@ export class SaveSequentializer { return !!this._pendingSave; } - public get pendingSave(): TPromise { + get pendingSave(): TPromise { return this._pendingSave ? this._pendingSave.promise : void 0; } - public setPending(versionId: number, promise: TPromise): TPromise { + setPending(versionId: number, promise: TPromise): TPromise { this._pendingSave = { versionId, promise }; - promise.done(() => this.donePending(versionId), () => this.donePending(versionId)); + promise.then(() => this.donePending(versionId), () => this.donePending(versionId)); return promise; } @@ -1076,11 +1095,11 @@ export class SaveSequentializer { this._nextSave = void 0; // Run next save and complete on the associated promise - saveOperation.run().done(saveOperation.promiseValue, saveOperation.promiseError); + saveOperation.run().then(saveOperation.promiseValue, saveOperation.promiseError); } } - public setNext(run: () => TPromise): TPromise { + setNext(run: () => TPromise): TPromise { // this is our first next save, so we create associated promise with it // so that we can return a promise that completes when the save operation @@ -1114,15 +1133,7 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler { constructor(@INotificationService private notificationService: INotificationService) { } - public onSaveError(error: any, model: TextFileEditorModel): void { + onSaveError(error: any, model: TextFileEditorModel): void { this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", path.basename(model.getResource().fsPath), toErrorMessage(error, false))); } } - -// Diagnostics support -let diag: (...args: any[]) => void; -if (!diag) { - diag = diagnostics.register('TextFileEditorModelDiagnostics', function (...args: any[]) { - console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])'); - }); -} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 7b19b9d0a36..1e42a100449 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -6,25 +6,40 @@ import { Event, Emitter, debounceEvent } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions, ILoadOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; +import { onUnexpectedError } from 'vs/base/common/errors'; -export class TextFileEditorModelManager implements ITextFileEditorModelManager { - private toUnbind: IDisposable[]; +export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { - private readonly _onModelDisposed: Emitter; - private readonly _onModelContentChanged: Emitter; - private readonly _onModelDirty: Emitter; - private readonly _onModelSaveError: Emitter; - private readonly _onModelSaved: Emitter; - private readonly _onModelReverted: Emitter; - private readonly _onModelEncodingChanged: Emitter; - private readonly _onModelOrphanedChanged: Emitter; + private readonly _onModelDisposed: Emitter = this._register(new Emitter()); + get onModelDisposed(): Event { return this._onModelDisposed.event; } + + private readonly _onModelContentChanged: Emitter = this._register(new Emitter()); + get onModelContentChanged(): Event { return this._onModelContentChanged.event; } + + private readonly _onModelDirty: Emitter = this._register(new Emitter()); + get onModelDirty(): Event { return this._onModelDirty.event; } + + private readonly _onModelSaveError: Emitter = this._register(new Emitter()); + get onModelSaveError(): Event { return this._onModelSaveError.event; } + + private readonly _onModelSaved: Emitter = this._register(new Emitter()); + get onModelSaved(): Event { return this._onModelSaved.event; } + + private readonly _onModelReverted: Emitter = this._register(new Emitter()); + get onModelReverted(): Event { return this._onModelReverted.event; } + + private readonly _onModelEncodingChanged: Emitter = this._register(new Emitter()); + get onModelEncodingChanged(): Event { return this._onModelEncodingChanged.event; } + + private readonly _onModelOrphanedChanged: Emitter = this._register(new Emitter()); + get onModelOrphanedChanged(): Event { return this._onModelOrphanedChanged.event; } private _onModelsDirtyEvent: Event; private _onModelsSaveError: Event; @@ -41,25 +56,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { @ILifecycleService private lifecycleService: ILifecycleService, @IInstantiationService private instantiationService: IInstantiationService ) { - this.toUnbind = []; - - this._onModelDisposed = new Emitter(); - this._onModelContentChanged = new Emitter(); - this._onModelDirty = new Emitter(); - this._onModelSaveError = new Emitter(); - this._onModelSaved = new Emitter(); - this._onModelReverted = new Emitter(); - this._onModelEncodingChanged = new Emitter(); - this._onModelOrphanedChanged = new Emitter(); - - this.toUnbind.push(this._onModelDisposed); - this.toUnbind.push(this._onModelContentChanged); - this.toUnbind.push(this._onModelDirty); - this.toUnbind.push(this._onModelSaveError); - this.toUnbind.push(this._onModelSaved); - this.toUnbind.push(this._onModelReverted); - this.toUnbind.push(this._onModelEncodingChanged); - this.toUnbind.push(this._onModelOrphanedChanged); + super(); this.mapResourceToModel = new ResourceMap(); this.mapResourceToDisposeListener = new ResourceMap(); @@ -76,39 +73,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { this.lifecycleService.onShutdown(this.dispose, this); } - public get onModelDisposed(): Event { - return this._onModelDisposed.event; - } - - public get onModelContentChanged(): Event { - return this._onModelContentChanged.event; - } - - public get onModelDirty(): Event { - return this._onModelDirty.event; - } - - public get onModelSaveError(): Event { - return this._onModelSaveError.event; - } - - public get onModelSaved(): Event { - return this._onModelSaved.event; - } - - public get onModelReverted(): Event { - return this._onModelReverted.event; - } - - public get onModelEncodingChanged(): Event { - return this._onModelEncodingChanged.event; - } - - public get onModelOrphanedChanged(): Event { - return this._onModelOrphanedChanged.event; - } - - public get onModelsDirty(): Event { + get onModelsDirty(): Event { if (!this._onModelsDirtyEvent) { this._onModelsDirtyEvent = this.debounce(this.onModelDirty); } @@ -116,7 +81,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this._onModelsDirtyEvent; } - public get onModelsSaveError(): Event { + get onModelsSaveError(): Event { if (!this._onModelsSaveError) { this._onModelsSaveError = this.debounce(this.onModelSaveError); } @@ -124,7 +89,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this._onModelsSaveError; } - public get onModelsSaved(): Event { + get onModelsSaved(): Event { if (!this._onModelsSaved) { this._onModelsSaved = this.debounce(this.onModelSaved); } @@ -132,7 +97,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this._onModelsSaved; } - public get onModelsReverted(): Event { + get onModelsReverted(): Event { if (!this._onModelsReverted) { this._onModelsReverted = this.debounce(this.onModelReverted); } @@ -155,11 +120,11 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return 250; } - public get(resource: URI): ITextFileEditorModel { + get(resource: URI): ITextFileEditorModel { return this.mapResourceToModel.get(resource); } - public loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise { + loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); @@ -167,18 +132,23 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return pendingLoad; } - let modelLoadOptions: ILoadOptions; - if (options && options.allowBinary) { - modelLoadOptions = { allowBinary: true }; - } - let modelPromise: TPromise; // Model exists let model = this.get(resource); if (model) { if (options && options.reload) { - modelPromise = model.load(modelLoadOptions); + + // async reload: trigger a reload but return immediately + if (options.reload.async) { + modelPromise = TPromise.as(model); + model.load(options).then(null, onUnexpectedError); + } + + // sync reload: do not return until model reloaded + else { + modelPromise = model.load(options); + } } else { modelPromise = TPromise.as(model); } @@ -187,7 +157,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { // Model does not exist else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0); - modelPromise = model.load(modelLoadOptions); + modelPromise = model.load(options); // Install state change listener this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => { @@ -249,7 +219,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { }); } - public getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] { + getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] { if (resource) { const res = this.mapResourceToModel.get(resource); @@ -266,7 +236,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return res; } - public add(resource: URI, model: ITextFileEditorModel): void { + add(resource: URI, model: ITextFileEditorModel): void { const knownModel = this.mapResourceToModel.get(resource); if (knownModel === model) { return; // already cached @@ -286,7 +256,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { })); } - public remove(resource: URI): void { + remove(resource: URI): void { this.mapResourceToModel.delete(resource); const disposeListener = this.mapResourceToDisposeListener.get(resource); @@ -308,7 +278,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { } } - public clear(): void { + clear(): void { // model caches this.mapResourceToModel.clear(); @@ -327,7 +297,7 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { this.mapResourceToModelContentChangeListener.clear(); } - public disposeModel(model: TextFileEditorModel): void { + disposeModel(model: TextFileEditorModel): void { if (!model) { return; // we need data! } @@ -346,8 +316,4 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { model.dispose(); } - - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); - } } \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 4c86dfd430c..484e14d38c3 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import * as errors from 'vs/base/common/errors'; import * as objects from 'vs/base/common/objects'; @@ -14,13 +14,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles'; +import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; @@ -33,6 +33,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/modelService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; export interface IBackupResult { didBackup: boolean; @@ -43,24 +44,26 @@ export interface IBackupResult { * * It also adds diagnostics and logging around file system operations. */ -export abstract class TextFileService implements ITextFileService { +export abstract class TextFileService extends Disposable implements ITextFileService { - public _serviceBrand: any; + _serviceBrand: any; + + private readonly _onFilesAssociationChange: Emitter = this._register(new Emitter()); + get onAutoSaveConfigurationChange(): Event { return this._onAutoSaveConfigurationChange.event; } + + private readonly _onAutoSaveConfigurationChange: Emitter = this._register(new Emitter()); + get onFilesAssociationChange(): Event { return this._onFilesAssociationChange.event; } + + private readonly _onWillMove = this._register(new Emitter()); + get onWillMove(): Event { return this._onWillMove.event; } - private toUnbind: IDisposable[]; private _models: TextFileEditorModelManager; - - private readonly _onFilesAssociationChange: Emitter; private currentFilesAssociationConfig: { [key: string]: string; }; - - private readonly _onAutoSaveConfigurationChange: Emitter; private configuredAutoSaveDelay: number; private configuredAutoSaveOnFocusChange: boolean; private configuredAutoSaveOnWindowChange: boolean; - - private autoSaveContext: IContextKey; - private configuredHotExit: string; + private autoSaveContext: IContextKey; constructor( private lifecycleService: ILifecycleService, @@ -77,13 +80,7 @@ export abstract class TextFileService implements ITextFileService { contextKeyService: IContextKeyService, private modelService: IModelService ) { - this.toUnbind = []; - - this._onAutoSaveConfigurationChange = new Emitter(); - this.toUnbind.push(this._onAutoSaveConfigurationChange); - - this._onFilesAssociationChange = new Emitter(); - this.toUnbind.push(this._onFilesAssociationChange); + super(); this._models = this.instantiationService.createInstance(TextFileEditorModelManager); this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService); @@ -96,7 +93,7 @@ export abstract class TextFileService implements ITextFileService { this.registerListeners(); } - public get models(): ITextFileEditorModelManager { + get models(): ITextFileEditorModelManager { return this._models; } @@ -106,14 +103,6 @@ export abstract class TextFileService implements ITextFileService { abstract confirmSave(resources?: URI[]): TPromise; - public get onAutoSaveConfigurationChange(): Event { - return this._onAutoSaveConfigurationChange.event; - } - - public get onFilesAssociationChange(): Event { - return this._onFilesAssociationChange.event; - } - private registerListeners(): void { // Lifecycle @@ -121,7 +110,7 @@ export abstract class TextFileService implements ITextFileService { this.lifecycleService.onShutdown(this.dispose, this); // Files configuration changes - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('files')) { this.onFilesConfigurationChange(this.configurationService.getValue()); } @@ -345,7 +334,7 @@ export abstract class TextFileService implements ITextFileService { // save all dirty when enabling auto save if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) { - this.saveAll().done(null, errors.onUnexpectedError); + this.saveAll(); } // Check for change in files associations @@ -364,7 +353,7 @@ export abstract class TextFileService implements ITextFileService { } } - public getDirty(resources?: URI[]): URI[] { + getDirty(resources?: URI[]): URI[] { // Collect files const dirty = this.getDirtyFileModels(resources).map(m => m.getResource()); @@ -375,7 +364,7 @@ export abstract class TextFileService implements ITextFileService { return dirty; } - public isDirty(resource?: URI): boolean { + isDirty(resource?: URI): boolean { // Check for dirty file if (this._models.getAll(resource).some(model => model.isDirty())) { @@ -386,7 +375,7 @@ export abstract class TextFileService implements ITextFileService { return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString()); } - public save(resource: URI, options?: ISaveOptions): TPromise { + save(resource: URI, options?: ISaveOptions): TPromise { // Run a forced save if we detect the file is not dirty so that save participants can still run if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) { @@ -399,9 +388,9 @@ export abstract class TextFileService implements ITextFileService { return this.saveAll([resource], options).then(result => result.results.length === 1 && result.results[0].success); } - public saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise; - public saveAll(resources: URI[], options?: ISaveOptions): TPromise; - public saveAll(arg1?: any, options?: ISaveOptions): TPromise { + saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise; + saveAll(resources: URI[], options?: ISaveOptions): TPromise; + saveAll(arg1?: any, options?: ISaveOptions): TPromise { // get all dirty let toSave: URI[] = []; @@ -435,16 +424,16 @@ export abstract class TextFileService implements ITextFileService { for (let i = 0; i < untitledResources.length; i++) { const untitled = untitledResources[i]; if (this.untitledEditorService.exists(untitled)) { - let targetPath: string; + let targetUri: URI; // Untitled with associated file path don't need to prompt if (this.untitledEditorService.hasAssociatedFilePath(untitled)) { - targetPath = untitled.fsPath; + targetUri = untitled.with({ scheme: Schemas.file }); } // Otherwise ask user else { - targetPath = await this.promptForPath(this.suggestFileName(untitled)); + const targetPath = await this.promptForPath(this.suggestFileName(untitled)); if (!targetPath) { return TPromise.as({ results: [...fileResources, ...untitledResources].map(r => { @@ -454,9 +443,11 @@ export abstract class TextFileService implements ITextFileService { }) }); } + + targetUri = URI.file(targetPath); } - targetsForUntitled.push(URI.file(targetPath)); + targetsForUntitled.push(targetUri); } } @@ -531,7 +522,7 @@ export abstract class TextFileService implements ITextFileService { return this.getFileModels(arg1).filter(model => model.isDirty()); } - public saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise { + saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise { // Get to target resource let targetPromise: TPromise; @@ -635,12 +626,12 @@ export abstract class TextFileService implements ITextFileService { private suggestFileName(untitledResource: URI): string { const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource); - const lastActiveFile = this.historyService.getLastActiveFile(); + const lastActiveFile = this.historyService.getLastActiveFile(Schemas.file); if (lastActiveFile) { return URI.file(paths.join(paths.dirname(lastActiveFile.fsPath), untitledFileName)).fsPath; } - const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot('file'); + const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(Schemas.file); if (lastActiveFolder) { return URI.file(paths.join(lastActiveFolder.fsPath, untitledFileName)).fsPath; } @@ -648,11 +639,11 @@ export abstract class TextFileService implements ITextFileService { return untitledFileName; } - public revert(resource: URI, options?: IRevertOptions): TPromise { + revert(resource: URI, options?: IRevertOptions): TPromise { return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success); } - public revertAll(resources?: URI[], options?: IRevertOptions): TPromise { + revertAll(resources?: URI[], options?: IRevertOptions): TPromise { // Revert files first return this.doRevertAllFiles(resources, options).then(operation => { @@ -701,7 +692,107 @@ export abstract class TextFileService implements ITextFileService { }); } - public getAutoSaveMode(): AutoSaveMode { + create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise { + const existingModel = this.models.get(resource); + + return this.fileService.createFile(resource, contents, options).then(() => { + + // If we had an existing model for the given resource, load + // it again to make sure it is up to date with the contents + // we just wrote into the underlying resource by calling + // revert() + if (existingModel && !existingModel.isDisposed()) { + return existingModel.revert(); + } + + return void 0; + }); + } + + delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise { + const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */)); + + return this.revertAll(dirtyFiles, { soft: true }).then(() => this.fileService.del(resource, options)); + } + + move(source: URI, target: URI, overwrite?: boolean): TPromise { + + const waitForPromises: TPromise[] = []; + this._onWillMove.fire({ + oldResource: source, + newResource: target, + waitUntil(p: Thenable) { + waitForPromises.push(TPromise.wrap(p).then(undefined, errors.onUnexpectedError)); + } + }); + + // prevent async waitUntil-calls + Object.freeze(waitForPromises); + + return TPromise.join(waitForPromises).then(() => { + + // Handle target models if existing (if target URI is a folder, this can be multiple) + let handleTargetModelPromise: TPromise = TPromise.as(void 0); + const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)); + if (dirtyTargetModels.length) { + handleTargetModelPromise = this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true }); + } + + return handleTargetModelPromise.then(() => { + + // Handle dirty source models if existing (if source URI is a folder, this can be multiple) + let handleDirtySourceModels: TPromise; + const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */)); + const dirtyTargetModels: URI[] = []; + if (dirtySourceModels.length) { + handleDirtySourceModels = TPromise.join(dirtySourceModels.map(sourceModel => { + const sourceModelResource = sourceModel.getResource(); + let targetModelResource: URI; + + // If the source is the actual model, just use target as new resource + if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) { + targetModelResource = target; + } + + // Otherwise a parent folder of the source is being moved, so we need + // to compute the target resource based on that + else { + targetModelResource = sourceModelResource.with({ path: paths.join(target.path, sourceModelResource.path.substr(source.path.length + 1)) }); + } + + // Remember as dirty target model to load after the operation + dirtyTargetModels.push(targetModelResource); + + // Backup dirty source model to the target resource it will become later + return this.backupFileService.backupResource(targetModelResource, sourceModel.createSnapshot(), sourceModel.getVersionId()); + })); + } else { + handleDirtySourceModels = TPromise.as(void 0); + } + + return handleDirtySourceModels.then(() => { + + // Soft revert the dirty source files if any + return this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true }).then(() => { + + // Rename to target + return this.fileService.moveFile(source, target, overwrite).then(() => { + + // Load models that were dirty before + return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel))).then(() => void 0); + }, error => { + + // In case of an error, discard any dirty target backups that were made + return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel))) + .then(() => TPromise.wrapError(error)); + }); + }); + }); + }); + }); + } + + getAutoSaveMode(): AutoSaveMode { if (this.configuredAutoSaveOnFocusChange) { return AutoSaveMode.ON_FOCUS_CHANGE; } @@ -717,7 +808,7 @@ export abstract class TextFileService implements ITextFileService { return AutoSaveMode.OFF; } - public getAutoSaveConfiguration(): IAutoSaveConfiguration { + getAutoSaveConfiguration(): IAutoSaveConfiguration { return { autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : void 0, autoSaveFocusChange: this.configuredAutoSaveOnFocusChange, @@ -725,14 +816,15 @@ export abstract class TextFileService implements ITextFileService { }; } - public get isHotExitEnabled(): boolean { + get isHotExitEnabled(): boolean { return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF; } - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); + dispose(): void { // Clear all caches this._models.clear(); + + super.dispose(); } } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 7bcde710cea..56719225de5 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; @@ -37,7 +37,7 @@ export interface ISaveParticipant { /** * States the text text file editor model can be in. */ -export enum ModelState { +export const enum ModelState { SAVED, DIRTY, PENDING_SAVE, @@ -60,7 +60,7 @@ export enum ModelState { ERROR } -export enum StateChange { +export const enum StateChange { DIRTY, SAVING, SAVE_ERROR, @@ -80,11 +80,11 @@ export class TextFileModelChangeEvent { this._kind = kind; } - public get resource(): URI { + get resource(): URI { return this._resource; } - public get kind(): StateChange { + get kind(): StateChange { return this._kind; } } @@ -108,7 +108,7 @@ export interface IAutoSaveConfiguration { autoSaveApplicationChange: boolean; } -export enum AutoSaveMode { +export const enum AutoSaveMode { OFF, AFTER_SHORT_DELAY, AFTER_LONG_DELAY, @@ -116,13 +116,19 @@ export enum AutoSaveMode { ON_WINDOW_CHANGE } -export enum SaveReason { +export const enum SaveReason { EXPLICIT = 1, AUTO = 2, FOCUS_CHANGE = 3, WINDOW_CHANGE = 4 } +export const enum LoadReason { + EDITOR = 1, + REFERENCE = 2, + OTHER = 3 +} + export const ITextFileService = createDecorator(TEXT_FILE_SERVICE_ID); export interface IRawTextContent extends IBaseStat { @@ -140,6 +146,10 @@ export interface IRawTextContent extends IBaseStat { export interface IModelLoadOrCreateOptions { + /** + * Context why the model is being loaded or created. + */ + reason?: LoadReason; /** * The encoding to use when resolving the model text content. @@ -147,9 +157,16 @@ export interface IModelLoadOrCreateOptions { encoding?: string; /** - * Wether to reload the model if it already exists. + * If the model was already loaded before, allows to trigger + * a reload of it to fetch the latest contents: + * - async: loadOrCreate() will return immediately and trigger + * a reload that will run in the background. + * - sync: loadOrCreate() will only return resolved when the + * model has finished reloading. */ - reload?: boolean; + reload?: { + async: boolean + }; /** * Allow to load a model even if we think it is a binary file. @@ -203,6 +220,11 @@ export interface ILoadOptions { * Allow to load a model even if we think it is a binary file. */ allowBinary?: boolean; + + /** + * Context why the model is being loaded. + */ + reason?: LoadReason; } export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { @@ -235,15 +257,27 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport isDisposed(): boolean; } + +export interface IWillMoveEvent { + oldResource: URI; + newResource: URI; + waitUntil(p: Thenable): void; +} + export interface ITextFileService extends IDisposable { _serviceBrand: any; - onAutoSaveConfigurationChange: Event; - onFilesAssociationChange: Event; + + readonly onAutoSaveConfigurationChange: Event; + readonly onFilesAssociationChange: Event; + + onWillMove: Event; + + readonly isHotExitEnabled: boolean; /** * Access to the manager of text file editor models providing further methods to work with them. */ - models: ITextFileEditorModelManager; + readonly models: ITextFileEditorModelManager; /** * Resolve the contents of a file identified by the resource. @@ -307,6 +341,22 @@ export interface ITextFileService extends IDisposable { */ revertAll(resources?: URI[], options?: IRevertOptions): TPromise; + /** + * Create a file. If the file exists it will be overwritten with the contents if + * the options enable to overwrite. + */ + create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise; + + /** + * Delete a file. If the file is dirty, it will get reverted and then deleted from disk. + */ + delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise; + + /** + * Move a file. If the file is dirty, its contents will be preserved and restored. + */ + move(source: URI, target: URI, overwrite?: boolean): TPromise; + /** * Brings up the confirm dialog to either save, don't save or cancel. * @@ -324,9 +374,4 @@ export interface ITextFileService extends IDisposable { * Convinient fast access to the raw configured auto save settings. */ getAutoSaveConfiguration(): IAutoSaveConfiguration; - - /** - * Convinient fast access to the hot exit file setting. - */ - isHotExitEnabled: boolean; } diff --git a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts index fcb157c8807..866527408d2 100644 --- a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as paths from 'vs/base/common/paths'; import * as strings from 'vs/base/common/strings'; import { isWindows } from 'vs/base/common/platform'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ConfirmResult } from 'vs/workbench/common/editor'; import { TextFileService as AbstractTextFileService } from 'vs/workbench/services/textfile/common/textFileService'; import { IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles'; @@ -54,7 +54,7 @@ export class TextFileService extends AbstractTextFileService { super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, notificationService, environmentService, backupFileService, windowsService, historyService, contextKeyService, modelService); } - public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { + resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { return this.fileService.resolveStreamContent(resource, options).then(streamContent => { return createTextBufferFactoryFromStream(streamContent.value).then(res => { const r: IRawTextContent = { @@ -63,6 +63,7 @@ export class TextFileService extends AbstractTextFileService { mtime: streamContent.mtime, etag: streamContent.etag, encoding: streamContent.encoding, + isReadonly: streamContent.isReadonly, value: res }; return r; @@ -70,7 +71,7 @@ export class TextFileService extends AbstractTextFileService { }); } - public confirmSave(resources?: URI[]): TPromise { + confirmSave(resources?: URI[]): TPromise { if (this.environmentService.isExtensionDevelopment) { return TPromise.wrap(ConfirmResult.DONT_SAVE); // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests) } @@ -101,7 +102,7 @@ export class TextFileService extends AbstractTextFileService { }); } - public promptForPath(defaultPath: string): TPromise { + promptForPath(defaultPath: string): TPromise { return this.windowService.showSaveDialog(this.getSaveDialogOptions(defaultPath)); } diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 5c0b1ab4dbe..67e5baa76d0 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -371,7 +371,7 @@ suite('Files - TextFileEditorModel', () => { let nextDone = false; const res = sequentializer.setNext(() => TPromise.as(null).then(() => { nextDone = true; return null; })); - return res.done(() => { + return res.then(() => { assert.ok(pendingDone); assert.ok(nextDone); }); @@ -387,7 +387,7 @@ suite('Files - TextFileEditorModel', () => { let nextDone = false; const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return null; })); - return res.done(() => { + return res.then(() => { assert.ok(pendingDone); assert.ok(nextDone); }); diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index b07a9c25b47..c64891e9263 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { join } from 'vs/base/common/paths'; @@ -105,7 +105,7 @@ suite('Files - TextFileEditorModelManager', () => { const resource = URI.file('/test.html'); const encoding = 'utf8'; - return manager.loadOrCreate(resource, { encoding, reload: true }).then(model => { + return manager.loadOrCreate(resource, { encoding }).then(model => { assert.ok(model); assert.equal(model.getEncoding(), encoding); assert.equal(manager.get(resource), model); diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts index e6d16134bfd..f133ab8b10c 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts @@ -5,10 +5,12 @@ 'use strict'; import * as assert from 'assert'; +import * as sinon from 'sinon'; import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ILifecycleService, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; -import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWindowsService } from 'vs/platform/windows/common/windows'; @@ -17,9 +19,12 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { ConfirmResult } from 'vs/workbench/common/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; -import { HotExitConfiguration } from 'vs/platform/files/common/files'; +import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { Schemas } from 'vs/base/common/network'; class ServiceAccessor { constructor( @@ -27,7 +32,9 @@ class ServiceAccessor { @ITextFileService public textFileService: TestTextFileService, @IUntitledEditorService public untitledEditorService: IUntitledEditorService, @IWindowsService public windowsService: TestWindowsService, - @IWorkspaceContextService public contextService: TestContextService + @IWorkspaceContextService public contextService: TestContextService, + @IModelService public modelService: ModelServiceImpl, + @IFileService public fileService: TestFileService ) { } } @@ -194,6 +201,34 @@ suite('Files - TextFileService', () => { }); }); + test('save - UNC path', function () { + const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' }); + model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8'); + (accessor.textFileService.models).add(model.getResource(), model); + + const mockedFileUri = untitledUncUri.with({ scheme: Schemas.file }); + const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8'); + const loadOrCreateStub = sinon.stub(accessor.textFileService.models, 'loadOrCreate', () => TPromise.wrap(mockedEditorInput)); + + sinon.stub(accessor.untitledEditorService, 'exists', () => true); + sinon.stub(accessor.untitledEditorService, 'hasAssociatedFilePath', () => true); + sinon.stub(accessor.modelService, 'updateModel', () => { }); + + return model.load().then(() => { + model.textEditorModel.setValue('foo'); + + return accessor.textFileService.saveAll(true).then(res => { + assert.ok(loadOrCreateStub.calledOnce); + assert.equal(res.results.length, 1); + assert.ok(res.results[0].success); + + assert.equal(res.results[0].target.scheme, Schemas.file); + assert.equal(res.results[0].target.authority, untitledUncUri.authority); + assert.equal(res.results[0].target.path, untitledUncUri.path); + }); + }); + }); + test('saveAll - file', function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); @@ -252,6 +287,45 @@ suite('Files - TextFileService', () => { }); }); + test('delete - dirty file', function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); + (accessor.textFileService.models).add(model.getResource(), model); + + const service = accessor.textFileService; + + return model.load().then(() => { + model.textEditorModel.setValue('foo'); + + assert.ok(service.isDirty(model.getResource())); + + return service.delete(model.getResource()).then(() => { + assert.ok(!service.isDirty(model.getResource())); + }); + }); + }); + + test('move - dirty file', function () { + let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8'); + let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8'); + (accessor.textFileService.models).add(sourceModel.getResource(), sourceModel); + (accessor.textFileService.models).add(targetModel.getResource(), targetModel); + + const service = accessor.textFileService; + + return sourceModel.load().then(() => { + sourceModel.textEditorModel.setValue('foo'); + + assert.ok(service.isDirty(sourceModel.getResource())); + + return service.move(sourceModel.getResource(), targetModel.getResource(), true).then(() => { + assert.ok(!service.isDirty(sourceModel.getResource())); + + sourceModel.dispose(); + targetModel.dispose(); + }); + }); + }); + suite('Hot Exit', () => { suite('"onExit" setting', () => { test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index c7617a56e68..f9075bfd380 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -5,14 +5,14 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { first } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as network from 'vs/base/common/network'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -31,16 +31,17 @@ class ResourceModelCollection extends ReferenceCollection { + createReferencedObject(key: string): TPromise { const resource = URI.parse(key); if (this.fileService.canHandleResource(resource)) { - return this.textFileService.models.loadOrCreate(resource); + return this.textFileService.models.loadOrCreate(resource, { reason: LoadReason.REFERENCE }); } + return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); } - public destroyReferencedObject(modelPromise: TPromise): void { - modelPromise.done(model => { + destroyReferencedObject(modelPromise: TPromise): void { + modelPromise.then(model => { if (model instanceof TextFileEditorModel) { this.textFileService.models.disposeModel(model); } else { @@ -51,7 +52,7 @@ class ResourceModelCollection extends ReferenceCollection { const resource = URI.parse(key); const providers = this.providers[resource.scheme] || []; - const factories = providers.map(p => () => p.provideTextContent(resource)); + const factories = providers.map(p => () => TPromise.wrap(p.provideTextContent(resource))); return first(factories).then(model => { if (!model) { - console.error(`Unable to open '${resource}' resource is not available.`); // TODO PII return TPromise.wrapError(new Error('resource is not available')); } @@ -108,21 +108,19 @@ export class TextModelResolverService implements ITextModelService { this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection); } - public createModelReference(resource: URI): TPromise> { + createModelReference(resource: URI): TPromise> { return this._createModelReference(resource); } private _createModelReference(resource: URI): TPromise> { // Untitled Schema: go through cached input - // TODO ImmortalReference is a hack if (resource.scheme === network.Schemas.untitled) { return this.untitledEditorService.loadOrCreate({ resource }).then(model => new ImmortalReference(model)); } // InMemory Schema: go through model service cache - // TODO ImmortalReference is a hack - if (resource.scheme === 'inmemory') { + if (resource.scheme === network.Schemas.inMemory) { const cachedModel = this.modelService.getModel(resource); if (!cachedModel) { @@ -144,7 +142,7 @@ export class TextModelResolverService implements ITextModelService { ); } - public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { + registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider); } } diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index 83b68a82c00..db48a7cf3cf 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { ITextModel } from 'vs/editor/common/model'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -133,12 +133,12 @@ suite('Workbench - TextModelResolverService', () => { let waitForIt = new TPromise(c => resolveModel = c); const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { - provideTextContent: async (resource: URI): TPromise => { - await waitForIt; - - let modelContent = 'Hello Test'; - let mode = accessor.modeService.getOrCreateMode('json'); - return accessor.modelService.createModel(modelContent, mode, resource); + provideTextContent: (resource: URI): TPromise => { + return waitForIt.then(_ => { + let modelContent = 'Hello Test'; + let mode = accessor.modeService.getOrCreateMode('json'); + return accessor.modelService.createModel(modelContent, mode, resource); + }); } }); diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 8d8a6df7b78..fb288d66132 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -59,6 +59,7 @@ export interface IWorkbenchThemeService extends IThemeService { getColorTheme(): IColorTheme; getColorThemes(): TPromise; onDidColorThemeChange: Event; + restoreColorTheme(); setFileIconTheme(iconThemeId: string, settingsTarget: ConfigurationTarget): TPromise; getFileIconTheme(): IFileIconTheme; diff --git a/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts b/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts index d125349a8fb..81c979f3f8a 100644 --- a/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts +++ b/src/vs/workbench/services/themes/electron-browser/colorThemeData.ts @@ -13,14 +13,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; - -import * as pfs from 'vs/base/node/pfs'; - +import * as resources from 'vs/base/common/resources'; import { Extensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ThemeType } from 'vs/platform/theme/common/themeService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { WorkbenchThemeService, IColorCustomizations } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; +import { IColorCustomizations } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; let colorRegistry = Registry.as(Extensions.ColorContribution); @@ -44,7 +44,7 @@ export class ColorThemeData implements IColorTheme { settingsId: string; description?: string; isLoaded: boolean; - path?: string; + location?: URI; extensionData: ExtensionData; get tokenColors(): ITokenColorizationRule[] { @@ -136,10 +136,10 @@ export class ColorThemeData implements IColorTheme { } } - public ensureLoaded(themeService: WorkbenchThemeService): TPromise { + public ensureLoaded(fileService: IFileService): TPromise { if (!this.isLoaded) { - if (this.path) { - return _loadColorThemeFromFile(this.path, this.themeTokenColors, this.colorMap).then(_ => { + if (this.location) { + return _loadColorTheme(fileService, this.location, this.themeTokenColors, this.colorMap).then(_ => { this.isLoaded = true; this.sanitizeTokenColors(); }); @@ -189,9 +189,12 @@ export class ColorThemeData implements IColorTheme { return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.tokenColors, other.tokenColors); } + get baseTheme(): string { + return this.id.split(' ')[0]; + } + get type(): ThemeType { - let baseTheme = this.id.split(' ')[0]; - switch (baseTheme) { + switch (this.baseTheme) { case VS_LIGHT_THEME: return 'light'; case VS_HC_THEME: return 'hc'; default: return 'dark'; @@ -244,7 +247,7 @@ export class ColorThemeData implements IColorTheme { } } - static fromExtensionTheme(theme: IThemeExtensionPoint, normalizedAbsolutePath: string, extensionData: ExtensionData): ColorThemeData { + static fromExtensionTheme(theme: IThemeExtensionPoint, colorThemeLocation: URI, extensionData: ExtensionData): ColorThemeData { let baseTheme: string = theme['uiTheme'] || 'vs-dark'; let themeSelector = toCSSSelector(extensionData.extensionId + '-' + Paths.normalize(theme.path)); @@ -253,7 +256,7 @@ export class ColorThemeData implements IColorTheme { themeData.label = theme.label || Paths.basename(theme.path); themeData.settingsId = theme.id || themeData.label; themeData.description = theme.description; - themeData.path = normalizedAbsolutePath; + themeData.location = colorThemeLocation; themeData.extensionData = extensionData; themeData.isLoaded = false; return themeData; @@ -270,17 +273,17 @@ function toCSSSelector(str: string) { return str; } -function _loadColorThemeFromFile(themePath: string, resultRules: ITokenColorizationRule[], resultColors: IColorMap): TPromise { - if (Paths.extname(themePath) === '.json') { - return pfs.readFile(themePath).then(content => { +function _loadColorTheme(fileService: IFileService, themeLocation: URI, resultRules: ITokenColorizationRule[], resultColors: IColorMap): TPromise { + if (Paths.extname(themeLocation.path) === '.json') { + return fileService.resolveContent(themeLocation, { encoding: 'utf8' }).then(content => { let errors: Json.ParseError[] = []; - let contentValue = Json.parse(content.toString(), errors); + let contentValue = Json.parse(content.value.toString(), errors); if (errors.length > 0) { return TPromise.wrapError(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', ')))); } let includeCompletes = TPromise.as(null); if (contentValue.include) { - includeCompletes = _loadColorThemeFromFile(Paths.join(Paths.dirname(themePath), contentValue.include), resultRules, resultColors); + includeCompletes = _loadColorTheme(fileService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), resultRules, resultColors); } return includeCompletes.then(_ => { if (Array.isArray(contentValue.settings)) { @@ -290,7 +293,7 @@ function _loadColorThemeFromFile(themePath: string, resultRules: ITokenColorizat let colors = contentValue.colors; if (colors) { if (typeof colors !== 'object') { - return TPromise.wrapError(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themePath))); + return TPromise.wrapError(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themeLocation.toString()))); } // new JSON color themes format for (let colorId in colors) { @@ -306,16 +309,16 @@ function _loadColorThemeFromFile(themePath: string, resultRules: ITokenColorizat resultRules.push(...tokenColors); return null; } else if (typeof tokenColors === 'string') { - return _loadSyntaxTokensFromFile(Paths.join(Paths.dirname(themePath), tokenColors), resultRules, {}); + return _loadSyntaxTokens(fileService, resources.joinPath(resources.dirname(themeLocation), tokenColors), resultRules, {}); } else { - return TPromise.wrapError(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themePath))); + return TPromise.wrapError(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString()))); } } return null; }); }); } else { - return _loadSyntaxTokensFromFile(themePath, resultRules, resultColors); + return _loadSyntaxTokens(fileService, themeLocation, resultRules, resultColors); } } @@ -324,11 +327,11 @@ function getPListParser() { return pListParser || import('fast-plist'); } -function _loadSyntaxTokensFromFile(themePath: string, resultRules: ITokenColorizationRule[], resultColors: IColorMap): TPromise { - return pfs.readFile(themePath).then(content => { +function _loadSyntaxTokens(fileService: IFileService, themeLocation: URI, resultRules: ITokenColorizationRule[], resultColors: IColorMap): TPromise { + return fileService.resolveContent(themeLocation, { encoding: 'utf8' }).then(content => { return getPListParser().then(parser => { try { - let contentValue = parser.parse(content.toString()); + let contentValue = parser.parse(content.value.toString()); let settings: ITokenColorizationRule[] = contentValue.settings; if (!Array.isArray(settings)) { return TPromise.wrapError(new Error(nls.localize('error.plist.invalidformat', "Problem parsing tmTheme file: {0}. 'settings' is not array."))); @@ -340,7 +343,7 @@ function _loadSyntaxTokensFromFile(themePath: string, resultRules: ITokenColoriz } }); }, error => { - return TPromise.wrapError(new Error(nls.localize('error.cannotload', "Problems loading tmTheme file {0}: {1}", themePath, error.message))); + return TPromise.wrapError(new Error(nls.localize('error.cannotload', "Problems loading tmTheme file {0}: {1}", themeLocation.toString(), error.message))); }); } diff --git a/src/vs/workbench/services/themes/electron-browser/colorThemeStore.ts b/src/vs/workbench/services/themes/electron-browser/colorThemeStore.ts index 8e31e971fcd..e6bbeb645ba 100644 --- a/src/vs/workbench/services/themes/electron-browser/colorThemeStore.ts +++ b/src/vs/workbench/services/themes/electron-browser/colorThemeStore.ts @@ -7,14 +7,14 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; -import * as Paths from 'path'; +import * as resources from 'vs/base/common/resources'; import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { IColorTheme, ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ColorThemeData } from 'vs/workbench/services/themes/electron-browser/colorThemeData'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; let themesExtPoint = ExtensionsRegistry.registerExtensionPoint('themes', [], { @@ -93,13 +93,13 @@ export class ColorThemeStore { )); return; } - // TODO@extensionLocation - let normalizedAbsolutePath = Paths.normalize(Paths.join(extensionLocation.fsPath, theme.path)); - if (normalizedAbsolutePath.indexOf(Paths.normalize(extensionLocation.fsPath)) !== 0) { - collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", themesExtPoint.name, normalizedAbsolutePath, extensionLocation.fsPath)); + const colorThemeLocation = resources.joinPath(extensionLocation, theme.path); + if (!resources.isEqualOrParent(colorThemeLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", themesExtPoint.name, colorThemeLocation.path, extensionLocation.path)); } - let themeData = ColorThemeData.fromExtensionTheme(theme, normalizedAbsolutePath, extensionData); + + let themeData = ColorThemeData.fromExtensionTheme(theme, colorThemeLocation, extensionData); if (themeData.id === this.extensionsColorThemes[0].id) { this.extensionsColorThemes[0] = themeData; } else { diff --git a/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts index 5f233d3ea3d..b6d8dd31b17 100644 --- a/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/electron-browser/fileIconThemeData.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as Paths from 'path'; +import * as resources from 'vs/base/common/resources'; import * as Json from 'vs/base/common/json'; import { ExtensionData, IThemeExtensionPoint, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as pfs from 'vs/base/node/pfs'; -import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; +import { IFileService } from 'vs/platform/files/common/files'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; export class FileIconThemeData implements IFileIconTheme { @@ -23,7 +23,7 @@ export class FileIconThemeData implements IFileIconTheme { hasFolderIcons?: boolean; hidesExplorerArrows?: boolean; isLoaded: boolean; - path?: string; + location?: URI; extensionData: ExtensionData; styleSheetContent?: string; @@ -31,11 +31,11 @@ export class FileIconThemeData implements IFileIconTheme { private constructor() { } - public ensureLoaded(themeService: WorkbenchThemeService): TPromise { + public ensureLoaded(fileService: IFileService): TPromise { if (!this.isLoaded) { - if (this.path) { - return _loadIconThemeDocument(this.path).then(iconThemeDocument => { - let result = _processIconThemeDocument(this.id, this.path, iconThemeDocument); + if (this.location) { + return _loadIconThemeDocument(fileService, this.location).then(iconThemeDocument => { + let result = _processIconThemeDocument(this.id, this.location, iconThemeDocument); this.styleSheetContent = result.content; this.hasFileIcons = result.hasFileIcons; this.hasFolderIcons = result.hasFolderIcons; @@ -48,13 +48,13 @@ export class FileIconThemeData implements IFileIconTheme { return TPromise.as(this.styleSheetContent); } - static fromExtensionTheme(iconTheme: IThemeExtensionPoint, normalizedAbsolutePath: string, extensionData: ExtensionData): FileIconThemeData { + static fromExtensionTheme(iconTheme: IThemeExtensionPoint, iconThemeLocation: URI, extensionData: ExtensionData): FileIconThemeData { let themeData = new FileIconThemeData(); themeData.id = extensionData.extensionId + '-' + iconTheme.id; themeData.label = iconTheme.label || Paths.basename(iconTheme.path); themeData.settingsId = iconTheme.id; themeData.description = iconTheme.description; - themeData.path = normalizedAbsolutePath; + themeData.location = iconThemeLocation; themeData.extensionData = extensionData; themeData.isLoaded = false; return themeData; @@ -89,13 +89,15 @@ export class FileIconThemeData implements IFileIconTheme { case 'description': case 'settingsId': case 'extensionData': - case 'path': case 'styleSheetContent': case 'hasFileIcons': case 'hidesExplorerArrows': case 'hasFolderIcons': theme[key] = data[key]; break; + case 'location': + theme.location = URI.revive(data.location); + break; } } return theme; @@ -110,7 +112,7 @@ export class FileIconThemeData implements IFileIconTheme { label: this.label, description: this.description, settingsId: this.settingsId, - path: this.path, + location: this.location, styleSheetContent: this.styleSheetContent, hasFileIcons: this.hasFileIcons, hasFolderIcons: this.hasFolderIcons, @@ -156,10 +158,10 @@ interface IconThemeDocument extends IconsAssociation { hidesExplorerArrows?: boolean; } -function _loadIconThemeDocument(fileSetPath: string): TPromise { - return pfs.readFile(fileSetPath).then(content => { +function _loadIconThemeDocument(fileService: IFileService, location: URI): TPromise { + return fileService.resolveContent(location, { encoding: 'utf8' }).then((content) => { let errors: Json.ParseError[] = []; - let contentValue = Json.parse(content.toString(), errors); + let contentValue = Json.parse(content.value.toString(), errors); if (errors.length > 0 || !contentValue) { return TPromise.wrapError(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing file icons file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', ')))); } @@ -167,7 +169,7 @@ function _loadIconThemeDocument(fileSetPath: string): TPromise('iconThemes', [], { description: nls.localize('vscode.extension.contributes.iconThemes', 'Contributes file icon themes.'), @@ -95,14 +95,13 @@ export class FileIconThemeStore { )); return; } - // TODO@extensionLocation - let normalizedAbsolutePath = Paths.normalize(Paths.join(extensionLocation.fsPath, iconTheme.path)); - if (normalizedAbsolutePath.indexOf(Paths.normalize(extensionLocation.fsPath)) !== 0) { - collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", iconThemeExtPoint.name, normalizedAbsolutePath, extensionLocation.fsPath)); + const iconThemeLocation = resources.joinPath(extensionLocation, iconTheme.path); + if (!resources.isEqualOrParent(iconThemeLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", iconThemeExtPoint.name, iconThemeLocation.path, extensionLocation.path)); } - let themeData = FileIconThemeData.fromExtensionTheme(iconTheme, normalizedAbsolutePath, extensionData); + let themeData = FileIconThemeData.fromExtensionTheme(iconTheme, iconThemeLocation, extensionData); this.knownIconThemes.push(themeData); }); diff --git a/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts index dc07f87f195..c6af43bc078 100644 --- a/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/electron-browser/workbenchThemeService.ts @@ -18,19 +18,17 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigu import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ColorThemeData } from './colorThemeData'; import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; -import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { Color } from 'vs/base/common/color'; import { Event, Emitter } from 'vs/base/common/event'; import * as colorThemeSchema from 'vs/workbench/services/themes/common/colorThemeSchema'; import * as fileIconThemeSchema from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService'; import { ColorThemeStore } from 'vs/workbench/services/themes/electron-browser/colorThemeStore'; import { FileIconThemeStore } from 'vs/workbench/services/themes/electron-browser/fileIconThemeStore'; import { FileIconThemeData } from 'vs/workbench/services/themes/electron-browser/fileIconThemeData'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { removeClasses, addClasses } from 'vs/base/browser/dom'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; // implementation @@ -70,6 +68,8 @@ export interface IColorCustomizations { export class WorkbenchThemeService implements IWorkbenchThemeService { _serviceBrand: any; + private fileService: IFileService; + private colorThemeStore: ColorThemeStore; private currentColorTheme: ColorThemeData; private container: HTMLElement; @@ -94,7 +94,6 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { container: HTMLElement, @IExtensionService extensionService: IExtensionService, @IStorageService private storageService: IStorageService, - @IBroadcastService private broadcastService: IBroadcastService, @IConfigurationService private configurationService: IConfigurationService, @ITelemetryService private telemetryService: ITelemetryService, @IWindowService private windowService: IWindowService, @@ -127,9 +126,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (persistedThemeData) { themeData = ColorThemeData.fromStorageData(persistedThemeData); } - if (!themeData) { - let isLightTheme = (Array.prototype.indexOf.call(document.body.classList, 'vs') >= 0); - themeData = ColorThemeData.createUnloadedTheme(isLightTheme ? VS_LIGHT_THEME : VS_DARK_THEME); + let containerBaseTheme = this.getBaseThemeFromContainer(); + if (!themeData || themeData && themeData.baseTheme !== containerBaseTheme) { + themeData = ColorThemeData.createUnloadedTheme(containerBaseTheme); } themeData.setCustomColors(this.colorCustomizations); themeData.setCustomTokenColors(this.tokenColorCustomizations); @@ -182,6 +181,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); } + acquireFileService(fileService: IFileService): void { + this.fileService = fileService; + } + public get onDidColorThemeChange(): Event { return this.onColorThemeChange.event; } @@ -278,7 +281,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(themeData => { if (themeData) { - return themeData.ensureLoaded(this).then(_ => { + return themeData.ensureLoaded(this.fileService).then(_ => { if (themeId === this.currentColorTheme.id && !this.currentColorTheme.isLoaded && this.currentColorTheme.hasEqualData(themeData)) { // the loaded theme is identical to the perisisted theme. Don't need to send an event. this.currentColorTheme = themeData; @@ -291,13 +294,24 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.updateDynamicCSSRules(themeData); return this.applyTheme(themeData, settingsTarget); }, error => { - return TPromise.wrapError(new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.path, error.message))); + return TPromise.wrapError(new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location.toString(), error.message))); }); } return null; }); } + public restoreColorTheme() { + let colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); + if (colorThemeSetting !== this.currentColorTheme.settingsId) { + this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, null).then(theme => { + if (theme) { + this.setColorTheme(theme.id, null); + } + }); + } + } + private updateDynamicCSSRules(themeData: ITheme) { let cssRules: string[] = []; let hasRule: { [rule: string]: boolean } = {}; @@ -335,11 +349,6 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.onColorThemeChange.fire(this.currentColorTheme); - if (settingsTarget !== ConfigurationTarget.WORKSPACE) { - let background = Color.Format.CSS.formatHex(newTheme.getColor(editorBackground)); // only take RGB, its what is used in the initial CSS - let data = { id: newTheme.id, background: background }; - this.broadcastService.broadcast({ channel: 'vscode:changeColorTheme', payload: JSON.stringify(data) }); - } // remember theme data for a quick restore this.storageService.store(PERSISTED_THEME_STORAGE_KEY, newTheme.toStorageData()); @@ -405,7 +414,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (!iconThemeData) { iconThemeData = FileIconThemeData.noIconTheme(); } - return iconThemeData.ensureLoaded(this).then(_ => { + return iconThemeData.ensureLoaded(this.fileService).then(_ => { return _applyIconTheme(iconThemeData, onApply); }); }); @@ -442,6 +451,18 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } return this._configurationWriter; } + + private getBaseThemeFromContainer() { + if (this.container) { + for (let i = this.container.classList.length - 1; i >= 0; i--) { + const item = document.body.classList.item(i); + if (item === VS_LIGHT_THEME || item === VS_DARK_THEME || item === VS_HC_THEME) { + return item; + } + } + } + return VS_DARK_THEME; + } } function _applyIconTheme(data: FileIconThemeData, onApply: (theme: FileIconThemeData) => TPromise): TPromise { diff --git a/src/vs/workbench/services/timer/common/timerService.ts b/src/vs/workbench/services/timer/common/timerService.ts deleted file mode 100644 index 84842c4eb00..00000000000 --- a/src/vs/workbench/services/timer/common/timerService.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const ITimerService = createDecorator('timerService'); - -/* __GDPR__FRAGMENT__ - "IMemoryInfo" : { - "workingSetSize" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "peakWorkingSetSize": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "privateBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "sharedBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } - } -*/ -export interface IMemoryInfo { - workingSetSize: number; - peakWorkingSetSize: number; - privateBytes: number; - sharedBytes: number; -} - -/* __GDPR__FRAGMENT__ - "IStartupMetrics" : { - "version" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "ellapsed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedAppReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedWindowLoad" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedWindowLoadToRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedExtensions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedExtensionsReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedViewletRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedEditorRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedWorkbench" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedTimersToTimersComputed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "timers.ellapsedNlsGeneration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "platform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "release" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "arch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totalmem" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "freemem" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "meminfo" : { "${inline}": [ "${IMemoryInfo}" ] }, - "cpus.count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "cpus.speed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "cpus.model" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "initialStartup" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "hasAccessibilitySupport" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "isVMLikelyhood" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "emptyWorkbench" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "loadavg" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -export interface IStartupMetrics { - version: number; - ellapsed: number; - timers: { - ellapsedAppReady?: number; - ellapsedWindowLoad?: number; - ellapsedWindowLoadToRequire: number; - ellapsedExtensions: number; - ellapsedExtensionsReady: number; - ellapsedRequire: number; - ellapsedViewletRestore: number; - ellapsedEditorRestore: number; - ellapsedWorkbench: number; - ellapsedTimersToTimersComputed: number; - ellapsedNlsGeneration: number; - }; - platform: string; - release: string; - arch: string; - totalmem: number; - freemem: number; - meminfo: IMemoryInfo; - cpus: { count: number; speed: number; model: string; }; - initialStartup: boolean; - hasAccessibilitySupport: boolean; - isVMLikelyhood: number; - emptyWorkbench: boolean; - loadavg: number[]; -} - -export interface IInitData { - start: number; - windowLoad: number; - isInitialStartup: boolean; - hasAccessibilitySupport: boolean; -} - -export interface ITimerService extends IInitData { - _serviceBrand: any; - - readonly startupMetrics: IStartupMetrics; -} diff --git a/src/vs/workbench/services/timer/electron-browser/timerService.ts b/src/vs/workbench/services/timer/electron-browser/timerService.ts new file mode 100644 index 00000000000..3134aa7889c --- /dev/null +++ b/src/vs/workbench/services/timer/electron-browser/timerService.ts @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { virtualMachineHint } from 'vs/base/node/id'; +import * as perf from 'vs/base/common/performance'; +import * as os from 'os'; +import { getAccessibilitySupport } from 'vs/base/browser/browser'; +import { AccessibilitySupport } from 'vs/base/common/platform'; +import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { IUpdateService } from 'vs/platform/update/common/update'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + + +/* __GDPR__FRAGMENT__ + "IMemoryInfo" : { + "workingSetSize" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "peakWorkingSetSize": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "privateBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "sharedBytes": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + } +*/ +export interface IMemoryInfo { + workingSetSize: number; + peakWorkingSetSize: number; + privateBytes: number; + sharedBytes: number; +} + +/* __GDPR__FRAGMENT__ + "IStartupMetrics" : { + "version" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "ellapsed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "isLatestVersion": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "didUseCachedData": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "windowKind": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "windowCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "viewletId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "panelId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editorIds": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "timers.ellapsedAppReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedWindowLoad" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedWindowLoadToRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedExtensions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedExtensionsReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedViewletRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedPanelRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedEditorRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedWorkbench" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedTimersToTimersComputed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedNlsGeneration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "platform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "release" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "arch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totalmem" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "freemem" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "meminfo" : { "${inline}": [ "${IMemoryInfo}" ] }, + "cpus.count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "cpus.speed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "cpus.model" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "initialStartup" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "hasAccessibilitySupport" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "isVMLikelyhood" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "emptyWorkbench" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "loadavg" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +export interface IStartupMetrics { + + /** + * The version of these metrics. + */ + version: 2; + + /** + * If this started the main process and renderer or just a renderer (new or reloaded). + */ + initialStartup: boolean; + + /** + * No folder, no file, no workspace has been opened + */ + emptyWorkbench: boolean; + + /** + * This is the latest (stable/insider) version. Iff not we should ignore this + * measurement. + */ + isLatestVersion: boolean; + + /** + * Whether we asked for and V8 accepted cached data. + */ + didUseCachedData: boolean; + + /** + * How/why the window was created. See https://github.com/Microsoft/vscode/blob/d1f57d871722f4d6ba63e4ef6f06287121ceb045/src/vs/platform/lifecycle/common/lifecycle.ts#L50 + */ + windowKind: number; + + /** + * The total number of windows that have been restored/created + */ + windowCount: number; + + /** + * The active viewlet id or `undedined` + */ + viewletId: string; + + /** + * The active panel id or `undefined` + */ + panelId: string; + + /** + * The editor input types or `[]` + */ + editorIds: string[]; + + /** + * The time it took to create the workbench. + * + * * Happens in the main-process *and* the renderer-process + * * Measured with the *start* and `didStartWorkbench`-performance mark. The *start* is either the start of the + * main process or the start of the renderer. + * * This should be looked at carefully because times vary depending on + * * This being the first window, the only window, or a reloaded window + * * Cached data being present and used or not + * * The numbers and types of editors being restored + * * The numbers of windows being restored (when starting 'fresh') + * * The viewlet being restored (esp. when it's a contributed viewlet) + */ + ellapsed: number; + + /** + * Individual timers... + */ + timers: { + /** + * The time it took to receieve the [`ready`](https://electronjs.org/docs/api/app#event-ready)-event. Measured from the first line + * of JavaScript code till receiving that event. + * + * * Happens in the main-process + * * Measured with the `main:started` and `main:appReady` performance marks. + * * This can be compared between insider and stable builds. + * * This should be looked at per OS version and per electron version. + * * This is often affected by AV software (and can change with AV software updates outside of our release-cycle). + * * It is not our code running here and we can only observe what's happening. + */ + ellapsedAppReady?: number; + + /** + * The time it took to generate NLS data. + * + * * Happens in the main-process + * * Measured with the `nlsGeneration:start` and `nlsGeneration:end` performance marks. + * * This only happens when a non-english locale is being used. + * * It is our code running here and we should monitor this carefully for regressions. + */ + ellapsedNlsGeneration: number; + + /** + * The time it took to tell electron to open/restore a renderer (browser window). + * + * * Happens in the main-process + * * Measured with the `main:appReady` and `main:loadWindow` performance marks. + * * This can be compared between insider and stable builds. + * * It is our code running here and we should monitor this carefully for regressions. + */ + ellapsedWindowLoad?: number; + + /** + * The time it took to create a new renderer (browser window) and to initialize that to the point + * of load the main-bundle (`workbench.main.js`). + * + * * Happens in the main-process *and* the renderer-process + * * Measured with the `main:loadWindow` and `willLoadWorkbenchMain` performance marks. + * * This can be compared between insider and stable builds. + * * It is mostly not our code running here and we can only observe what's happening. + * + */ + ellapsedWindowLoadToRequire: number; + + /** + * The time it took to load the main-bundle of the workbench, e.g `workbench.main.js`. + * + * * Happens in the renderer-process + * * Measured with the `willLoadWorkbenchMain` and `didLoadWorkbenchMain` performance marks. + * * This varies *a lot* when V8 cached data could be used or not + * * This should be looked at with and without V8 cached data usage and per electron/v8 version + * * This is affected by the size of our code bundle (which grows about 3-5% per release) + */ + ellapsedRequire: number; + + /** + * The time it took to read extensions' package.json-files *and* interpret them (invoking + * the contribution points). + * + * * Happens in the renderer-process + * * Measured with the `willLoadExtensions` and `didLoadExtensions` performance marks. + * * Reading of package.json-files is avoided by caching them all in a single file (after the read, + * until another extension is installed) + * * Happens in parallel to other things, depends on async timing + * + * todo@joh/ramya this measures an artifical dealy we have added, see https://github.com/Microsoft/vscode/blob/2f07ddae8bf56e969e3f4ba1447258ebc999672f/src/vs/workbench/services/extensions/electron-browser/extensionService.ts#L311-L326 + */ + ellapsedExtensions: number; + + // the time from start till `didLoadExtensions` + // remove? + ellapsedExtensionsReady: number; + + /** + * The time it took to restore the viewlet. + * + * * Happens in the renderer-process + * * Measured with the `willRestoreViewlet` and `didRestoreViewlet` performance marks. + * * This should be looked at per viewlet-type/id. + * * Happens in parallel to other things, depends on async timing + */ + ellapsedViewletRestore: number; + + /** + * The time it took to restore the panel. + * + * * Happens in the renderer-process + * * Measured with the `willRestorePanel` and `didRestorePanel` performance marks. + * * This should be looked at per panel-type/id. + * * Happens in parallel to other things, depends on async timing + */ + ellapsedPanelRestore: number; + + /** + * The time it took to restore editors - that is text editor and complex editor likes the settings UI + * or webviews (markdown preview). + * + * * Happens in the renderer-process + * * Measured with the `willRestoreEditors` and `didRestoreEditors` performance marks. + * * This should be looked at per editor and per editor type. + * * Happens in parallel to other things, depends on async timing + * + * todo@joh/ramya We should probably measures each editor individually? + */ + ellapsedEditorRestore: number; + + /** + * The time it took to create the workbench. + * + * * Happens in the renderer-process + * * Measured with the `willStartWorkbench` and `didStartWorkbench` performance marks. + * + * todo@joh/ramya Not sure if this is useful because this includes too much + */ + ellapsedWorkbench: number; + + // the time it took to generate this object. + // remove? + ellapsedTimersToTimersComputed: number; + }; + + hasAccessibilitySupport: boolean; + isVMLikelyhood: number; + platform: string; + release: string; + arch: string; + totalmem: number; + freemem: number; + meminfo: IMemoryInfo; + cpus: { count: number; speed: number; model: string; }; + loadavg: number[]; +} + +export interface ITimerService { + _serviceBrand: any; + readonly startupMetrics: Promise; +} + +class TimerService implements ITimerService { + + _serviceBrand: any; + + private _startupMetrics: Promise; + + constructor( + @IWindowsService private readonly _windowsService: IWindowsService, + @IWindowService private readonly _windowService: IWindowService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IUpdateService private readonly _updateService: IUpdateService, + @IViewletService private readonly _viewletService: IViewletService, + @IPanelService private readonly _panelService: IPanelService, + @IEditorService private readonly _editorService: IEditorService, + ) { + } + + get startupMetrics(): Promise { + if (!this._startupMetrics) { + this._startupMetrics = Promise + .resolve(this._extensionService.whenInstalledExtensionsRegistered()) + .then(() => this._computeStartupMetrics()); + } + return this._startupMetrics; + } + + private async _computeStartupMetrics(): Promise { + + const now = Date.now(); + const initialStartup = !!this._windowService.getConfiguration().isInitialStartup; + const startMark = initialStartup ? 'main:started' : 'main:loadWindow'; + + let totalmem: number; + let freemem: number; + let cpus: { count: number; speed: number; model: string; }; + let platform: string; + let release: string; + let arch: string; + let loadavg: number[]; + let meminfo: IMemoryInfo; + let isVMLikelyhood: number; + + try { + totalmem = os.totalmem(); + freemem = os.freemem(); + platform = os.platform(); + release = os.release(); + arch = os.arch(); + loadavg = os.loadavg(); + meminfo = process.getProcessMemoryInfo(); + + isVMLikelyhood = Math.round((virtualMachineHint.value() * 100)); + + const rawCpus = os.cpus(); + if (rawCpus && rawCpus.length > 0) { + cpus = { count: rawCpus.length, speed: rawCpus[0].speed, model: rawCpus[0].model }; + } + } catch (error) { + // ignore, be on the safe side with these hardware method calls + } + + return { + version: 2, + ellapsed: perf.getDuration(startMark, 'didStartWorkbench'), + + // reflections + isLatestVersion: Boolean(await this._updateService.isLatestVersion()), + didUseCachedData: didUseCachedData(), + windowKind: this._lifecycleService.startupKind, + windowCount: await this._windowsService.getWindowCount(), + viewletId: this._viewletService.getActiveViewlet() ? this._viewletService.getActiveViewlet().getId() : undefined, + editorIds: this._editorService.visibleEditors.map(input => input.getTypeId()), + panelId: this._panelService.getActivePanel() ? this._panelService.getActivePanel().getId() : undefined, + + // timers + timers: { + ellapsedAppReady: initialStartup ? perf.getDuration('main:started', 'main:appReady') : undefined, + ellapsedNlsGeneration: initialStartup ? perf.getDuration('nlsGeneration:start', 'nlsGeneration:end') : undefined, + ellapsedWindowLoad: initialStartup ? perf.getDuration('main:appReady', 'main:loadWindow') : undefined, + ellapsedWindowLoadToRequire: perf.getDuration('main:loadWindow', 'willLoadWorkbenchMain'), + ellapsedRequire: perf.getDuration('willLoadWorkbenchMain', 'didLoadWorkbenchMain'), + ellapsedExtensions: perf.getDuration('willLoadExtensions', 'didLoadExtensions'), + ellapsedEditorRestore: perf.getDuration('willRestoreEditors', 'didRestoreEditors'), + ellapsedViewletRestore: perf.getDuration('willRestoreViewlet', 'didRestoreViewlet'), + ellapsedPanelRestore: perf.getDuration('willRestorePanel', 'didRestorePanel'), + ellapsedWorkbench: perf.getDuration('willStartWorkbench', 'didStartWorkbench'), + ellapsedExtensionsReady: perf.getDuration(startMark, 'didLoadExtensions'), + ellapsedTimersToTimersComputed: Date.now() - now, + }, + + // system info + platform, + release, + arch, + totalmem, + freemem, + meminfo, + cpus, + loadavg, + initialStartup, + isVMLikelyhood, + hasAccessibilitySupport: getAccessibilitySupport() === AccessibilitySupport.Enabled, + emptyWorkbench: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY + }; + } +} + +export const ITimerService = createDecorator('timerService'); + +registerSingleton(ITimerService, TimerService); + +//#region cached data logic + +export function didUseCachedData(): boolean { + // We surely don't use cached data when we don't tell the loader to do so + if (!Boolean((global).require.getConfig().nodeCachedDataDir)) { + return false; + } + // whenever cached data is produced or rejected a onNodeCachedData-callback is invoked. That callback + // stores data in the `MonacoEnvironment.onNodeCachedData` global. See: + // https://github.com/Microsoft/vscode/blob/efe424dfe76a492eab032343e2fa4cfe639939f0/src/vs/workbench/electron-browser/bootstrap/index.js#L299 + if (!isFalsyOrEmpty(MonacoEnvironment.onNodeCachedData)) { + return false; + } + return true; +} + +declare type OnNodeCachedDataArgs = [{ errorCode: string, path: string, detail?: string }, { path: string, length: number }]; +declare const MonacoEnvironment: { onNodeCachedData: OnNodeCachedDataArgs[] }; + +//#endregion diff --git a/src/vs/workbench/services/timer/node/timerService.ts b/src/vs/workbench/services/timer/node/timerService.ts deleted file mode 100644 index 1de3ba8f6cc..00000000000 --- a/src/vs/workbench/services/timer/node/timerService.ts +++ /dev/null @@ -1,109 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { ITimerService, IStartupMetrics, IInitData, IMemoryInfo } from 'vs/workbench/services/timer/common/timerService'; -import { virtualMachineHint } from 'vs/base/node/id'; -import * as perf from 'vs/base/common/performance'; -import * as os from 'os'; - -export class TimerService implements ITimerService { - - public _serviceBrand: any; - - public readonly start: number; - public readonly windowLoad: number; - - public readonly isInitialStartup: boolean; - public readonly hasAccessibilitySupport: boolean; - - private _startupMetrics: IStartupMetrics; - - constructor(initData: IInitData, private isEmptyWorkbench: boolean) { - this.start = initData.start; - this.windowLoad = initData.windowLoad; - - this.isInitialStartup = initData.isInitialStartup; - this.hasAccessibilitySupport = initData.hasAccessibilitySupport; - } - - get startupMetrics(): IStartupMetrics { - if (!this._startupMetrics) { - this._computeStartupMetrics(); - } - return this._startupMetrics; - } - - public _computeStartupMetrics(): void { - const now = Date.now(); - const initialStartup = !!this.isInitialStartup; - const start = initialStartup ? this.start : this.windowLoad; - - let totalmem: number; - let freemem: number; - let cpus: { count: number; speed: number; model: string; }; - let platform: string; - let release: string; - let arch: string; - let loadavg: number[]; - let meminfo: IMemoryInfo; - let isVMLikelyhood: number; - - try { - totalmem = os.totalmem(); - freemem = os.freemem(); - platform = os.platform(); - release = os.release(); - arch = os.arch(); - loadavg = os.loadavg(); - meminfo = process.getProcessMemoryInfo(); - - isVMLikelyhood = Math.round((virtualMachineHint.value() * 100)); - - const rawCpus = os.cpus(); - if (rawCpus && rawCpus.length > 0) { - cpus = { count: rawCpus.length, speed: rawCpus[0].speed, model: rawCpus[0].model }; - } - } catch (error) { - // ignore, be on the safe side with these hardware method calls - } - - let nlsStart = perf.getEntry('mark', 'nlsGeneration:start'); - let nlsEnd = perf.getEntry('mark', 'nlsGeneration:end'); - let nlsTime = nlsStart && nlsEnd ? nlsEnd.startTime - nlsStart.startTime : 0; - this._startupMetrics = { - version: 1, - ellapsed: perf.getEntry('mark', 'didStartWorkbench').startTime - start, - timers: { - ellapsedExtensions: perf.getDuration('willLoadExtensions', 'didLoadExtensions'), - ellapsedExtensionsReady: perf.getEntry('mark', 'didLoadExtensions').startTime - start, - ellapsedRequire: perf.getDuration('willLoadWorkbenchMain', 'didLoadWorkbenchMain'), - ellapsedEditorRestore: perf.getDuration('willRestoreEditors', 'didRestoreEditors'), - ellapsedViewletRestore: perf.getDuration('willRestoreViewlet', 'didRestoreViewlet'), - ellapsedWorkbench: perf.getDuration('willStartWorkbench', 'didStartWorkbench'), - ellapsedWindowLoadToRequire: perf.getEntry('mark', 'willLoadWorkbenchMain').startTime - this.windowLoad, - ellapsedTimersToTimersComputed: Date.now() - now, - ellapsedNlsGeneration: nlsTime - }, - platform, - release, - arch, - totalmem, - freemem, - meminfo, - cpus, - loadavg, - initialStartup, - isVMLikelyhood, - hasAccessibilitySupport: !!this.hasAccessibilitySupport, - emptyWorkbench: this.isEmptyWorkbench - }; - - if (initialStartup) { - this._startupMetrics.timers.ellapsedAppReady = perf.getDuration('main:started', 'main:appReady'); - this._startupMetrics.timers.ellapsedWindowLoad = this.windowLoad - perf.getEntry('mark', 'main:appReady').startTime; - } - } -} diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index b667bea963a..d2aff3db294 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as arrays from 'vs/base/common/arrays'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; @@ -15,6 +15,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { TPromise } from 'vs/base/common/winjs.base'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { Schemas } from 'vs/base/common/network'; +import { Disposable } from 'vs/base/common/lifecycle'; export const IUntitledEditorService = createDecorator('untitledEditorService'); @@ -104,42 +105,30 @@ export interface IUntitledEditorService { getEncoding(resource: URI): string; } -export class UntitledEditorService implements IUntitledEditorService { +export class UntitledEditorService extends Disposable implements IUntitledEditorService { - public _serviceBrand: any; + _serviceBrand: any; private mapResourceToInput = new ResourceMap(); private mapResourceToAssociatedFilePath = new ResourceMap(); - private readonly _onDidChangeContent: Emitter; - private readonly _onDidChangeDirty: Emitter; - private readonly _onDidChangeEncoding: Emitter; - private readonly _onDidDisposeModel: Emitter; + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + get onDidChangeContent(): Event { return this._onDidChangeContent.event; } + + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } + + private readonly _onDidChangeEncoding: Emitter = this._register(new Emitter()); + get onDidChangeEncoding(): Event { return this._onDidChangeEncoding.event; } + + private readonly _onDidDisposeModel: Emitter = this._register(new Emitter()); + get onDidDisposeModel(): Event { return this._onDidDisposeModel.event; } constructor( @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService ) { - this._onDidChangeContent = new Emitter(); - this._onDidChangeDirty = new Emitter(); - this._onDidChangeEncoding = new Emitter(); - this._onDidDisposeModel = new Emitter(); - } - - public get onDidDisposeModel(): Event { - return this._onDidDisposeModel.event; - } - - public get onDidChangeContent(): Event { - return this._onDidChangeContent.event; - } - - public get onDidChangeDirty(): Event { - return this._onDidChangeDirty.event; - } - - public get onDidChangeEncoding(): Event { - return this._onDidChangeEncoding.event; + super(); } protected get(resource: URI): UntitledEditorInput { @@ -154,11 +143,11 @@ export class UntitledEditorService implements IUntitledEditorService { return this.mapResourceToInput.values(); } - public exists(resource: URI): boolean { + exists(resource: URI): boolean { return this.mapResourceToInput.has(resource); } - public revertAll(resources?: URI[], force?: boolean): URI[] { + revertAll(resources?: URI[], force?: boolean): URI[] { const reverted: URI[] = []; const untitledInputs = this.getAll(resources); @@ -174,13 +163,13 @@ export class UntitledEditorService implements IUntitledEditorService { return reverted; } - public isDirty(resource: URI): boolean { + isDirty(resource: URI): boolean { const input = this.get(resource); return input && input.isDirty(); } - public getDirty(resources?: URI[]): URI[] { + getDirty(resources?: URI[]): URI[] { let inputs: UntitledEditorInput[]; if (resources) { inputs = resources.map(r => this.get(r)).filter(i => !!i); @@ -193,11 +182,11 @@ export class UntitledEditorService implements IUntitledEditorService { .map(i => i.getResource()); } - public loadOrCreate(options: IModelLoadOrCreateOptions = Object.create(null)): TPromise { + loadOrCreate(options: IModelLoadOrCreateOptions = Object.create(null)): TPromise { return this.createOrGet(options.resource, options.modeId, options.initialValue, options.encoding, options.useResourcePath).resolve(); } - public createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput { + createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput { if (resource) { // Massage resource if it comes with a file:// scheme @@ -274,26 +263,19 @@ export class UntitledEditorService implements IUntitledEditorService { return input; } - public hasAssociatedFilePath(resource: URI): boolean { + hasAssociatedFilePath(resource: URI): boolean { return this.mapResourceToAssociatedFilePath.has(resource); } - public suggestFileName(resource: URI): string { + suggestFileName(resource: URI): string { const input = this.get(resource); return input ? input.suggestFileName() : void 0; } - public getEncoding(resource: URI): string { + getEncoding(resource: URI): string { const input = this.get(resource); return input ? input.getEncoding() : void 0; } - - public dispose(): void { - this._onDidChangeContent.dispose(); - this._onDidChangeDirty.dispose(); - this._onDidChangeEncoding.dispose(); - this._onDidDisposeModel.dispose(); - } } diff --git a/src/vs/workbench/services/viewlet/browser/viewlet.ts b/src/vs/workbench/services/viewlet/browser/viewlet.ts index 02006a2397a..a6903bb9fb9 100644 --- a/src/vs/workbench/services/viewlet/browser/viewlet.ts +++ b/src/vs/workbench/services/viewlet/browser/viewlet.ts @@ -41,6 +41,11 @@ export interface IViewletService { */ getViewlet(id: string): ViewletDescriptor; + /** + * Returns all viewlets + */ + getAllViewlets(): ViewletDescriptor[]; + /** * Returns all enabled viewlets */ diff --git a/src/vs/workbench/services/viewlet/browser/viewletService.ts b/src/vs/workbench/services/viewlet/browser/viewletService.ts index 354410b5820..30f05343f48 100644 --- a/src/vs/workbench/services/viewlet/browser/viewletService.ts +++ b/src/vs/workbench/services/viewlet/browser/viewletService.ts @@ -13,40 +13,41 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { Disposable } from 'vs/base/common/lifecycle'; const ActiveViewletContextId = 'activeViewlet'; export const ActiveViewletContext = new RawContextKey(ActiveViewletContextId, ''); -export class ViewletService implements IViewletService { +export class ViewletService extends Disposable implements IViewletService { - public _serviceBrand: any; + _serviceBrand: any; private sidebarPart: SidebarPart; private viewletRegistry: ViewletRegistry; private activeViewletContextKey: IContextKey; private _onDidViewletEnable = new Emitter<{ id: string, enabled: boolean }>(); - private disposables: IDisposable[] = []; - public get onDidViewletRegister(): Event { return >this.viewletRegistry.onDidRegister; } - public get onDidViewletOpen(): Event { return this.sidebarPart.onDidViewletOpen; } - public get onDidViewletClose(): Event { return this.sidebarPart.onDidViewletClose; } - public get onDidViewletEnablementChange(): Event<{ id: string, enabled: boolean }> { return this._onDidViewletEnable.event; } + get onDidViewletRegister(): Event { return >this.viewletRegistry.onDidRegister; } + get onDidViewletOpen(): Event { return this.sidebarPart.onDidViewletOpen; } + get onDidViewletClose(): Event { return this.sidebarPart.onDidViewletClose; } + get onDidViewletEnablementChange(): Event<{ id: string, enabled: boolean }> { return this._onDidViewletEnable.event; } constructor( sidebarPart: SidebarPart, @IContextKeyService contextKeyService: IContextKeyService, @IExtensionService private extensionService: IExtensionService ) { + super(); + this.sidebarPart = sidebarPart; this.viewletRegistry = Registry.as(ViewletExtensions.Viewlets); this.activeViewletContextKey = ActiveViewletContext.bindTo(contextKeyService); - this.onDidViewletOpen(this._onDidViewletOpen, this, this.disposables); - this.onDidViewletClose(this._onDidViewletClose, this, this.disposables); + this._register(this.onDidViewletOpen(this._onDidViewletOpen, this)); + this._register(this.onDidViewletClose(this._onDidViewletClose, this)); } private _onDidViewletOpen(viewlet: IViewlet): void { @@ -61,7 +62,7 @@ export class ViewletService implements IViewletService { } } - public setViewletEnablement(id: string, enabled: boolean): void { + setViewletEnablement(id: string, enabled: boolean): void { const descriptor = this.getAllViewlets().filter(desc => desc.id === id).pop(); if (descriptor && descriptor.enabled !== enabled) { descriptor.enabled = enabled; @@ -69,7 +70,7 @@ export class ViewletService implements IViewletService { } } - public openViewlet(id: string, focus?: boolean): TPromise { + openViewlet(id: string, focus?: boolean): TPromise { if (this.getViewlet(id)) { return this.sidebarPart.openViewlet(id, focus); } @@ -82,33 +83,29 @@ export class ViewletService implements IViewletService { }); } - public getActiveViewlet(): IViewlet { + getActiveViewlet(): IViewlet { return this.sidebarPart.getActiveViewlet(); } - public getViewlets(): ViewletDescriptor[] { + getViewlets(): ViewletDescriptor[] { return this.getAllViewlets() .filter(v => v.enabled); } - private getAllViewlets(): ViewletDescriptor[] { + getAllViewlets(): ViewletDescriptor[] { return this.viewletRegistry.getViewlets() .sort((v1, v2) => v1.order - v2.order); } - public getDefaultViewletId(): string { + getDefaultViewletId(): string { return this.viewletRegistry.getDefaultViewletId(); } - public getViewlet(id: string): ViewletDescriptor { + getViewlet(id: string): ViewletDescriptor { return this.getViewlets().filter(viewlet => viewlet.id === id)[0]; } - public getProgressIndicator(id: string): IProgressService { + getProgressIndicator(id: string): IProgressService { return this.sidebarPart.getProgressIndicator(id); } - - dispose(): void { - this.disposables = dispose(this.disposables); - } } diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 920be3ef667..1c88e14cff2 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; export const IWorkspaceEditingService = createDecorator('workspaceEditingService'); @@ -33,6 +33,11 @@ export interface IWorkspaceEditingService { */ updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise; + /** + * enters the workspace with the provided path. + */ + enterWorkspace(path: string): TPromise; + /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 5e8f0fb6ca9..732af2d8dc1 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -6,7 +6,7 @@ 'use strict'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -18,7 +18,7 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/node/confi import { migrateStorageToMultiRootWorkspace } from 'vs/platform/storage/common/migration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { StorageService } from 'vs/platform/storage/common/storageService'; -import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -31,7 +31,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ export class WorkspaceEditingService implements IWorkspaceEditingService { - public _serviceBrand: any; + _serviceBrand: any; constructor( @IJSONEditingService private jsonEditingService: IJSONEditingService, @@ -46,7 +46,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ) { } - public updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise { + updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise { const folders = this.contextService.getWorkspace().folders; let foldersToDelete: URI[] = []; @@ -96,7 +96,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } - public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): TPromise { + addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): TPromise { return this.doAddFolders(foldersToAdd, void 0, donotNotifyError); } @@ -122,7 +122,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } - public removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): TPromise { + removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): TPromise { // If we are in single-folder state and the opened folder is to be removed, // we create an empty workspace and enter it. @@ -138,17 +138,21 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { private includesSingleFolderWorkspace(folders: URI[]): boolean { if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { const workspaceFolder = this.contextService.getWorkspace().folders[0]; - return (folders.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))); + return (folders.some(folder => isEqual(folder, workspaceFolder.uri))); } return false; } - public createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { + enterWorkspace(path: string): TPromise { + return this.doEnterWorkspace(() => this.windowService.enterWorkspace(path)); + } + + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); } - public saveAndEnterWorkspace(path: string): TPromise { + saveAndEnterWorkspace(path: string): TPromise { return this.doEnterWorkspace(() => this.windowService.saveAndEnterWorkspace(path)); } @@ -188,9 +192,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Stop the extension host first to give extensions most time to shutdown this.extensionService.stopExtensionHost(); + let extensionHostStarted: boolean = false; const startExtensionHost = () => { this.extensionService.startExtensionHost(); + extensionHostStarted = true; }; return mainSidePromise().then(result => { @@ -199,22 +205,22 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (result) { return this.migrate(result.workspace).then(() => { - // TODO@Ben TODO@Sandeep the following requires ugly casts and should probably have a service interface - // Reinitialize backup service - const backupFileService = this.backupFileService as BackupFileService; - backupFileService.initialize(result.backupPath); + if (this.backupFileService instanceof BackupFileService) { + this.backupFileService.initialize(result.backupPath); + } // Reinitialize configuration service const workspaceImpl = this.contextService as WorkspaceService; - return workspaceImpl.initialize(result.workspace); + return workspaceImpl.initialize(result.workspace, startExtensionHost); }); } return TPromise.as(void 0); - }).then(startExtensionHost, error => { - startExtensionHost(); // in any case start the extension host again! - + }).then(null, error => { + if (!extensionHostStarted) { + startExtensionHost(); // start the extension host if not started + } return TPromise.wrapError(error); }); } @@ -226,7 +232,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Settings migration (only if we come from a folder workspace) if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - return this.copyWorkspaceSettings(toWorkspace); + return this.migrateWorkspaceSettings(toWorkspace); } return TPromise.as(void 0); @@ -240,11 +246,23 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { storageImpl.setWorkspaceId(newWorkspaceId); } - public copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): TPromise { + private migrateWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): TPromise { + return this.doCopyWorkspaceSettings(toWorkspace, setting => setting.scope === ConfigurationScope.WINDOW); + } + + copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): TPromise { + return this.doCopyWorkspaceSettings(toWorkspace); + } + + private doCopyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier, filter?: (config: IConfigurationPropertySchema) => boolean): TPromise { const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const targetWorkspaceConfiguration = {}; for (const key of this.workspaceConfigurationService.keys().workspace) { - if (configurationProperties[key] && !configurationProperties[key].notMultiRootAdopted && configurationProperties[key].scope === ConfigurationScope.WINDOW) { + if (configurationProperties[key]) { + if (filter && !filter(configurationProperties[key])) { + continue; + } + targetWorkspaceConfiguration[key] = this.workspaceConfigurationService.inspect(key).workspace; } } diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 22c8a969e55..c4a78a473c6 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -6,13 +6,13 @@ 'use strict'; import * as assert from 'assert'; -import { Builder, $ } from 'vs/base/browser/builder'; import { Part } from 'vs/workbench/browser/part'; import * as Types from 'vs/base/common/types'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { append, $, hide } from 'vs/base/browser/dom'; class MyPart extends Part { @@ -42,21 +42,21 @@ class MyPart2 extends Part { } public createTitleArea(parent: HTMLElement): HTMLElement { - return $(parent).div(function (div) { - div.span({ - id: 'myPart.title', - innerHtml: 'Title' - }); - }).getHTMLElement(); + const titleContainer = append(parent, $('div')); + const titleLabel = append(titleContainer, $('span')); + titleLabel.id = 'myPart.title'; + titleLabel.innerHTML = 'Title'; + + return titleContainer; } public createContentArea(parent: HTMLElement): HTMLElement { - return $(parent).div(function (div) { - div.span({ - id: 'myPart.content', - innerHtml: 'Content' - }); - }).getHTMLElement(); + const contentContainer = append(parent, $('div')); + const contentSpan = append(contentContainer, $('span')); + contentSpan.id = 'myPart.content'; + contentSpan.innerHTML = 'Content'; + + return contentContainer; } } @@ -71,12 +71,12 @@ class MyPart3 extends Part { } public createContentArea(parent: HTMLElement): HTMLElement { - return $(parent).div(function (div) { - div.span({ - id: 'myPart.content', - innerHtml: 'Content' - }); - }).getHTMLElement(); + const contentContainer = append(parent, $('div')); + const contentSpan = append(contentContainer, $('span')); + contentSpan.id = 'myPart.content'; + contentSpan.innerHTML = 'Content'; + + return contentContainer; } } @@ -97,11 +97,12 @@ suite('Workbench parts', () => { }); test('Creation', function () { - let b = new Builder(document.getElementById(fixtureId)); - b.div().hide(); + let b = document.createElement('div'); + document.getElementById(fixtureId).appendChild(b); + hide(b); - let part = new MyPart(b.getHTMLElement()); - part.create(b.getHTMLElement()); + let part = new MyPart(b); + part.create(b); assert.strictEqual(part.getId(), 'myPart'); @@ -114,7 +115,7 @@ suite('Workbench parts', () => { part.shutdown(); // Re-Create to assert memento contents - part = new MyPart(b.getHTMLElement()); + part = new MyPart(b); memento = part.getMemento(storage); assert(memento); @@ -126,29 +127,31 @@ suite('Workbench parts', () => { delete memento.bar; part.shutdown(); - part = new MyPart(b.getHTMLElement()); + part = new MyPart(b); memento = part.getMemento(storage); assert(memento); assert.strictEqual(Types.isEmptyObject(memento), true); }); test('Part Layout with Title and Content', function () { - let b = new Builder(document.getElementById(fixtureId)); - b.div().hide(); + let b = document.createElement('div'); + document.getElementById(fixtureId).appendChild(b); + hide(b); let part = new MyPart2(); - part.create(b.getHTMLElement()); + part.create(b); assert(document.getElementById('myPart.title')); assert(document.getElementById('myPart.content')); }); test('Part Layout with Content only', function () { - let b = new Builder(document.getElementById(fixtureId)); - b.div().hide(); + let b = document.createElement('div'); + document.getElementById(fixtureId).appendChild(b); + hide(b); let part = new MyPart3(); - part.create(b.getHTMLElement()); + part.create(b); assert(!document.getElementById('myPart.title')); assert(document.getElementById('myPart.content')); diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index 2f2a863607d..b6d674c9226 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorOptions, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -14,12 +14,14 @@ import * as Platform from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { workbenchInstantiationService, TestEditorGroup } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestEditorGroup, TestEditorGroupsService } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IEditorRegistry, Extensions, EditorDescriptor } from 'vs/workbench/browser/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; const NullThemeService = new TestThemeService(); @@ -69,7 +71,7 @@ class MyInput extends EditorInput { return ''; } - resolve(refresh?: boolean): any { + resolve(): any { return null; } } @@ -79,7 +81,7 @@ class MyOtherInput extends EditorInput { return ''; } - resolve(refresh?: boolean): any { + resolve(): any { return null; } } @@ -188,6 +190,116 @@ suite('Workbench base editor', () => { assert(factory); }); + test('EditorMemento - basics', function () { + const testGroup0 = new TestEditorGroup(0); + const testGroup1 = new TestEditorGroup(1); + const testGroup4 = new TestEditorGroup(4); + + const editorGroupService = new TestEditorGroupsService([ + testGroup0, + testGroup1, + new TestEditorGroup(2) + ]); + + interface TestViewState { + line: number; + } + + const rawMemento = Object.create(null); + let memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService); + + let res = memento.loadState(testGroup0, URI.file('/A')); + assert.ok(!res); + + memento.saveState(testGroup0, URI.file('/A'), { line: 3 }); + res = memento.loadState(testGroup0, URI.file('/A')); + assert.ok(res); + assert.equal(res.line, 3); + + memento.saveState(testGroup1, URI.file('/A'), { line: 5 }); + res = memento.loadState(testGroup1, URI.file('/A')); + assert.ok(res); + assert.equal(res.line, 5); + + // Ensure capped at 3 elements + memento.saveState(testGroup0, URI.file('/B'), { line: 1 }); + memento.saveState(testGroup0, URI.file('/C'), { line: 1 }); + memento.saveState(testGroup0, URI.file('/D'), { line: 1 }); + memento.saveState(testGroup0, URI.file('/E'), { line: 1 }); + + assert.ok(!memento.loadState(testGroup0, URI.file('/A'))); + assert.ok(!memento.loadState(testGroup0, URI.file('/B'))); + assert.ok(memento.loadState(testGroup0, URI.file('/C'))); + assert.ok(memento.loadState(testGroup0, URI.file('/D'))); + assert.ok(memento.loadState(testGroup0, URI.file('/E'))); + + // Save at an unknown group + memento.saveState(testGroup4, URI.file('/E'), { line: 1 }); + assert.ok(memento.loadState(testGroup4, URI.file('/E'))); // only gets removed when memento is saved + memento.saveState(testGroup4, URI.file('/C'), { line: 1 }); + assert.ok(memento.loadState(testGroup4, URI.file('/C'))); // only gets removed when memento is saved + + memento.shutdown(); + + memento = new EditorMemento('id', 'key', rawMemento, 3, editorGroupService); + assert.ok(memento.loadState(testGroup0, URI.file('/C'))); + assert.ok(memento.loadState(testGroup0, URI.file('/D'))); + assert.ok(memento.loadState(testGroup0, URI.file('/E'))); + + // Check on entries no longer there from invalid groups + assert.ok(!memento.loadState(testGroup4, URI.file('/E'))); + assert.ok(!memento.loadState(testGroup4, URI.file('/C'))); + + memento.clearState(URI.file('/C')); + memento.clearState(URI.file('/E')); + + assert.ok(!memento.loadState(testGroup0, URI.file('/C'))); + assert.ok(memento.loadState(testGroup0, URI.file('/D'))); + assert.ok(!memento.loadState(testGroup0, URI.file('/E'))); + }); + + test('EditoMemento - use with editor input', function () { + const testGroup0 = new TestEditorGroup(0); + + interface TestViewState { + line: number; + } + + class TestEditorInput extends EditorInput { + constructor(private resource: URI, private id = 'testEditorInput') { + super(); + } + public getTypeId() { return 'testEditorInput'; } + public resolve(): TPromise { return null; } + + public matches(other: TestEditorInput): boolean { + return other && this.id === other.id && other instanceof TestEditorInput; + } + + public getResource(): URI { + return this.resource; + } + } + + const rawMemento = Object.create(null); + let memento = new EditorMemento('id', 'key', rawMemento, 3, new TestEditorGroupsService()); + + const testInputA = new TestEditorInput(URI.file('/A')); + + let res = memento.loadState(testGroup0, testInputA); + assert.ok(!res); + + memento.saveState(testGroup0, testInputA, { line: 3 }); + res = memento.loadState(testGroup0, testInputA); + assert.ok(res); + assert.equal(res.line, 3); + + // State removed when input gets disposed + testInputA.dispose(); + res = memento.loadState(testGroup0, testInputA); + assert.ok(!res); + }); + return { MyEditor: MyEditor, MyOtherEditor: MyOtherEditor diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts new file mode 100644 index 00000000000..185cf678818 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { FileKind } from 'vs/platform/files/common/files'; + + +suite('Breadcrumb Model', function () { + + const workspaceService = new TestContextService(new Workspace('ffff', [new WorkspaceFolder({ uri: URI.parse('foo:/bar/baz/ws'), name: 'ws', index: 0 })])); + const configService = new class extends TestConfigurationService { + getValue(...args: any[]) { + if (args[0] === 'breadcrumbs.filePath') { + return 'on'; + } + if (args[0] === 'breadcrumbs.symbolPath') { + return 'on'; + } + return super.getValue(...args); + } + }; + + test('only uri, inside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, workspaceService, configService); + let elements = model.getElements(); + + assert.equal(elements.length, 3); + let [one, two, three] = elements as FileElement[]; + assert.equal(one.kind, FileKind.FOLDER); + assert.equal(two.kind, FileKind.FOLDER); + assert.equal(three.kind, FileKind.FILE); + assert.equal(one.uri.toString(), 'foo:/bar/baz/ws/some'); + assert.equal(two.uri.toString(), 'foo:/bar/baz/ws/some/path'); + assert.equal(three.uri.toString(), 'foo:/bar/baz/ws/some/path/file.ts'); + }); + + test('only uri, outside workspace', function () { + + let model = new EditorBreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, workspaceService, configService); + let elements = model.getElements(); + + assert.equal(elements.length, 2); + let [one, two] = elements as FileElement[]; + assert.equal(one.kind, FileKind.FOLDER); + assert.equal(two.kind, FileKind.FILE); + assert.equal(one.uri.toString(), 'foo:/outside'); + assert.equal(two.uri.toString(), 'foo:/outside/file.ts'); + }); +}); diff --git a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts index e395c7acabc..4fdcb2b7cef 100644 --- a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestEditorService } from 'vs/workbench/test/workbenchTestServices'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; diff --git a/src/vs/workbench/test/browser/quickopen.test.ts b/src/vs/workbench/test/browser/quickopen.test.ts index fd71405603a..d17bd4fdc81 100644 --- a/src/vs/workbench/test/browser/quickopen.test.ts +++ b/src/vs/workbench/test/browser/quickopen.test.ts @@ -22,10 +22,6 @@ export class TestQuickOpenService implements IQuickOpenService { this.callback = callback; } - pick(arg: any, options?: any, token?: any): Promise { - return TPromise.as(null); - } - accept(): void { } diff --git a/src/vs/workbench/test/common/editor/dataUriEditorInput.test.ts b/src/vs/workbench/test/common/editor/dataUriEditorInput.test.ts index 8fd2d6f25b6..d77f3d29cc6 100644 --- a/src/vs/workbench/test/common/editor/dataUriEditorInput.test.ts +++ b/src/vs/workbench/test/common/editor/dataUriEditorInput.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; diff --git a/src/vs/workbench/test/common/editor/editor.test.ts b/src/vs/workbench/test/common/editor/editor.test.ts index 18f03391a6a..f729c337ccf 100644 --- a/src/vs/workbench/test/common/editor/editor.test.ts +++ b/src/vs/workbench/test/common/editor/editor.test.ts @@ -7,13 +7,13 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import { EditorInput, toResource, EditorViewStateMemento } from 'vs/workbench/common/editor'; +import { EditorInput, toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService, TestEditorGroupsService, TestEditorGroup } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; class ServiceAccessor { @@ -35,7 +35,7 @@ class FileEditorInput extends EditorInput { return this.resource; } - resolve(refresh?: boolean): TPromise { + resolve(): TPromise { return TPromise.as(null); } } @@ -86,114 +86,4 @@ suite('Workbench editor', () => { assert.equal(toResource(file, { supportSideBySide: true, filter: Schemas.file }).toString(), file.getResource().toString()); assert.equal(toResource(file, { supportSideBySide: true, filter: [Schemas.file, Schemas.untitled] }).toString(), file.getResource().toString()); }); - - test('EditorViewStateMemento - basics', function () { - const testGroup0 = new TestEditorGroup(0); - const testGroup1 = new TestEditorGroup(1); - const testGroup4 = new TestEditorGroup(4); - - const editorGroupService = new TestEditorGroupsService([ - testGroup0, - testGroup1, - new TestEditorGroup(2) - ]); - - interface TestViewState { - line: number; - } - - const rawMemento = Object.create(null); - let memento = new EditorViewStateMemento(editorGroupService, rawMemento, 'key', 3); - - let res = memento.loadState(testGroup0, URI.file('/A')); - assert.ok(!res); - - memento.saveState(testGroup0, URI.file('/A'), { line: 3 }); - res = memento.loadState(testGroup0, URI.file('/A')); - assert.ok(res); - assert.equal(res.line, 3); - - memento.saveState(testGroup1, URI.file('/A'), { line: 5 }); - res = memento.loadState(testGroup1, URI.file('/A')); - assert.ok(res); - assert.equal(res.line, 5); - - // Ensure capped at 3 elements - memento.saveState(testGroup0, URI.file('/B'), { line: 1 }); - memento.saveState(testGroup0, URI.file('/C'), { line: 1 }); - memento.saveState(testGroup0, URI.file('/D'), { line: 1 }); - memento.saveState(testGroup0, URI.file('/E'), { line: 1 }); - - assert.ok(!memento.loadState(testGroup0, URI.file('/A'))); - assert.ok(!memento.loadState(testGroup0, URI.file('/B'))); - assert.ok(memento.loadState(testGroup0, URI.file('/C'))); - assert.ok(memento.loadState(testGroup0, URI.file('/D'))); - assert.ok(memento.loadState(testGroup0, URI.file('/E'))); - - // Save at an unknown group - memento.saveState(testGroup4, URI.file('/E'), { line: 1 }); - assert.ok(memento.loadState(testGroup4, URI.file('/E'))); // only gets removed when memento is saved - memento.saveState(testGroup4, URI.file('/C'), { line: 1 }); - assert.ok(memento.loadState(testGroup4, URI.file('/C'))); // only gets removed when memento is saved - - memento.save(); - - memento = new EditorViewStateMemento(editorGroupService, rawMemento, 'key', 3); - assert.ok(memento.loadState(testGroup0, URI.file('/C'))); - assert.ok(memento.loadState(testGroup0, URI.file('/D'))); - assert.ok(memento.loadState(testGroup0, URI.file('/E'))); - - // Check on entries no longer there from invalid groups - assert.ok(!memento.loadState(testGroup4, URI.file('/E'))); - assert.ok(!memento.loadState(testGroup4, URI.file('/C'))); - - memento.clearState(URI.file('/C')); - memento.clearState(URI.file('/E')); - - assert.ok(!memento.loadState(testGroup0, URI.file('/C'))); - assert.ok(memento.loadState(testGroup0, URI.file('/D'))); - assert.ok(!memento.loadState(testGroup0, URI.file('/E'))); - }); - - test('EditorViewStateMemento - use with editor input', function () { - const testGroup0 = new TestEditorGroup(0); - - interface TestViewState { - line: number; - } - - class TestEditorInput extends EditorInput { - constructor(private resource: URI, private id = 'testEditorInput') { - super(); - } - public getTypeId() { return 'testEditorInput'; } - public resolve(): TPromise { return null; } - - public matches(other: TestEditorInput): boolean { - return other && this.id === other.id && other instanceof TestEditorInput; - } - - public getResource(): URI { - return this.resource; - } - } - - const rawMemento = Object.create(null); - let memento = new EditorViewStateMemento(new TestEditorGroupsService(), rawMemento, 'key', 3); - - const testInputA = new TestEditorInput(URI.file('/A')); - - let res = memento.loadState(testGroup0, testInputA); - assert.ok(!res); - - memento.saveState(testGroup0, testInputA, { line: 3 }); - res = memento.loadState(testGroup0, testInputA); - assert.ok(res); - assert.equal(res.line, 3); - - // State removed when input gets disposed - testInputA.dispose(); - res = memento.loadState(testGroup0, testInputA); - assert.ok(!res); - }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts index b4ce15928a4..1c9dba44a0a 100644 --- a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts @@ -11,7 +11,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; @@ -55,7 +55,7 @@ suite('Workbench editor model', () => { let otherInput = instantiationService.createInstance(ResourceEditorInput, 'name2', 'description', URI.from({ scheme: 'test', authority: null, path: 'thePath' })); let diffInput = new DiffEditorInput('name', 'description', input, otherInput); - return diffInput.resolve(true).then((model: any) => { + return diffInput.resolve().then((model: any) => { assert(model); assert(model instanceof TextDiffEditorModel); @@ -63,7 +63,7 @@ suite('Workbench editor model', () => { assert(diffEditorModel.original); assert(diffEditorModel.modified); - return diffInput.resolve(true).then((model: any) => { + return diffInput.resolve().then((model: any) => { assert(model.isResolved()); assert(diffEditorModel !== model.textDiffEditorModel); diff --git a/src/vs/workbench/test/common/editor/editorGroups.test.ts b/src/vs/workbench/test/common/editor/editorGroups.test.ts index d29a9cd41e8..1400512cbd5 100644 --- a/src/vs/workbench/test/common/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/common/editor/editorGroups.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { EditorGroup, ISerializedEditorGroup, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroup'; import { Extensions as EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputFactory, CloseDirection } from 'vs/workbench/common/editor'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TestStorageService, TestLifecycleService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; diff --git a/src/vs/workbench/test/common/editor/editorInput.test.ts b/src/vs/workbench/test/common/editor/editorInput.test.ts index b162d802d25..94eea4a8805 100644 --- a/src/vs/workbench/test/common/editor/editorInput.test.ts +++ b/src/vs/workbench/test/common/editor/editorInput.test.ts @@ -11,7 +11,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; class MyEditorInput extends EditorInput { getTypeId(): string { return ''; } - resolve(refresh?: boolean): any { return null; } + resolve(): any { return null; } } suite('Workbench editor input', () => { diff --git a/src/vs/workbench/test/common/editor/editorModel.test.ts b/src/vs/workbench/test/common/editor/editorModel.test.ts index b1f991b36fe..180f11cb122 100644 --- a/src/vs/workbench/test/common/editor/editorModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorModel.test.ts @@ -16,7 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ITextBufferFactory } from 'vs/editor/common/model'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; class MyEditorModel extends EditorModel { } @@ -24,6 +24,10 @@ class MyTextEditorModel extends BaseTextEditorModel { public createTextEditorModel(value: ITextBufferFactory, resource?: URI, modeId?: string) { return super.createTextEditorModel(value, resource, modeId); } + + isReadonly(): boolean { + return false; + } } suite('Workbench editor model', () => { diff --git a/src/vs/workbench/test/common/editor/editorOptions.test.ts b/src/vs/workbench/test/common/editor/editorOptions.test.ts index f5dd20c857f..345b58f4833 100644 --- a/src/vs/workbench/test/common/editor/editorOptions.test.ts +++ b/src/vs/workbench/test/common/editor/editorOptions.test.ts @@ -16,12 +16,12 @@ suite('Workbench editor options', () => { assert(!options.preserveFocus); options.preserveFocus = true; assert(options.preserveFocus); - assert(!options.forceOpen); - options.forceOpen = true; - assert(options.forceOpen); + assert(!options.forceReload); + options.forceReload = true; + assert(options.forceReload); options = new EditorOptions(); - options.forceOpen = true; + options.forceReload = true; }); test('TextEditorOptions', function () { @@ -35,7 +35,7 @@ suite('Workbench editor options', () => { otherOptions.selection(1, 1, 2, 2); options = new TextEditorOptions(); - options.forceOpen = true; + options.forceReload = true; options.selection(1, 1, 2, 2); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts index f08815e0582..44a0eff301d 100644 --- a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/test/common/editor/untitledEditor.test.ts b/src/vs/workbench/test/common/editor/untitledEditor.test.ts index 3f21eeb388a..814aa29645c 100644 --- a/src/vs/workbench/test/common/editor/untitledEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledEditor.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as assert from 'assert'; import { join } from 'vs/base/common/paths'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/test/electron-browser/api/extHost.api.impl.test.ts b/src/vs/workbench/test/electron-browser/api/extHost.api.impl.test.ts new file mode 100644 index 00000000000..299caf29e7d --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/extHost.api.impl.test.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { originalFSPath } from 'vs/workbench/api/node/extHost.api.impl'; + +suite('ExtHost API', function () { + test('issue #51387: originalFSPath', function () { + assert.equal(originalFSPath(URI.file('C:\\test')).charAt(0), 'C'); + assert.equal(originalFSPath(URI.file('c:\\test')).charAt(0), 'c'); + + assert.equal(originalFSPath(URI.revive(JSON.parse(JSON.stringify(URI.file('C:\\test'))))).charAt(0), 'C'); + assert.equal(originalFSPath(URI.revive(JSON.parse(JSON.stringify(URI.file('c:\\test'))))).charAt(0), 'c'); + }); +}); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 34528d0623d..a1be3772424 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import * as types from 'vs/workbench/api/node/extHostTypes'; import { TextModel as EditorModel } from 'vs/editor/common/model/textModel'; @@ -122,7 +122,7 @@ suite('ExtHostLanguageFeatureCommands', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics, new NullLogService()); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); @@ -332,12 +332,44 @@ suite('ExtHostLanguageFeatureCommands', function () { return commands.executeCommand('vscode.executeDocumentSymbolProvider', model.uri).then(values => { assert.equal(values.length, 2); let [first, second] = values; + assert.ok(first instanceof types.SymbolInformation); + assert.ok(second instanceof types.SymbolInformation); assert.equal(first.name, 'testing2'); assert.equal(second.name, 'testing1'); }); }); }); + test('vscode.executeDocumentSymbolProvider command only returns SymbolInformation[] rather than DocumentSymbol[] #57984', function () { + disposables.push(extHost.registerDocumentSymbolProvider(defaultSelector, { + provideDocumentSymbols(): any { + return [ + new types.SymbolInformation('SymbolInformation', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0)) + ]; + } + })); + disposables.push(extHost.registerDocumentSymbolProvider(defaultSelector, { + provideDocumentSymbols(): any { + let root = new types.DocumentSymbol('DocumentSymbol', 'DocumentSymbol#detail', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0), new types.Range(1, 0, 1, 0)); + root.children = [new types.DocumentSymbol('DocumentSymbol#child', 'DocumentSymbol#detail#child', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0), new types.Range(1, 0, 1, 0))]; + return [root]; + } + })); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand<(vscode.SymbolInformation & vscode.DocumentSymbol)[]>('vscode.executeDocumentSymbolProvider', model.uri).then(values => { + assert.equal(values.length, 2); + let [first, second] = values; + assert.ok(first instanceof types.SymbolInformation); + assert.ok(!(first instanceof types.DocumentSymbol)); + assert.ok(second instanceof types.SymbolInformation); + assert.equal(first.name, 'DocumentSymbol'); + assert.equal(first.children.length, 1); + assert.equal(second.name, 'SymbolInformation'); + }); + }); + }); + // --- suggest test('Suggest, back and forth', function () { @@ -448,6 +480,65 @@ suite('ExtHostLanguageFeatureCommands', function () { }); + test('"vscode.executeCompletionItemProvider" doesnot return a preselect field #53749', async function () { + disposables.push(extHost.registerCompletionItemProvider(defaultSelector, { + provideCompletionItems(): any { + let a = new types.CompletionItem('item1'); + a.preselect = true; + let b = new types.CompletionItem('item2'); + let c = new types.CompletionItem('item3'); + c.preselect = true; + let d = new types.CompletionItem('item4'); + return new types.CompletionList([a, b, c, d], false); + } + }, [])); + + await rpcProtocol.sync(); + + let list = await commands.executeCommand( + 'vscode.executeCompletionItemProvider', + model.uri, + new types.Position(0, 4), + undefined + ); + + assert.ok(list instanceof types.CompletionList); + assert.equal(list.items.length, 4); + + let [a, b, c, d] = list.items; + assert.equal(a.preselect, true); + assert.equal(b.preselect, undefined); + assert.equal(c.preselect, true); + assert.equal(d.preselect, undefined); + }); + + test('executeCompletionItemProvider doesn\'t capture commitCharacters #58228', async function () { + disposables.push(extHost.registerCompletionItemProvider(defaultSelector, { + provideCompletionItems(): any { + let a = new types.CompletionItem('item1'); + a.commitCharacters = ['a', 'b']; + let b = new types.CompletionItem('item2'); + return new types.CompletionList([a, b], false); + } + }, [])); + + await rpcProtocol.sync(); + + let list = await commands.executeCommand( + 'vscode.executeCompletionItemProvider', + model.uri, + new types.Position(0, 4), + undefined + ); + + assert.ok(list instanceof types.CompletionList); + assert.equal(list.items.length, 2); + + let [a, b] = list.items; + assert.deepEqual(a.commitCharacters, ['a', 'b']); + assert.equal(b.commitCharacters, undefined); + }); + // --- quickfix test('QuickFix, back and forth', function () { @@ -633,4 +724,20 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); }); + + test('"TypeError: e.onCancellationRequested is not a function" calling hover provider in Insiders #54174', function () { + + disposables.push(extHost.registerHoverProvider(defaultSelector, { + provideHover(): any { + return new types.Hover('fofofofo'); + } + })); + + return rpcProtocol.sync().then(() => { + return commands.executeCommand('vscode.executeHoverProvider', model.uri, new types.Position(1, 1)).then(value => { + assert.equal(value.length, 1); + assert.equal(value[0].contents.length, 1); + }); + }); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts index 6638722bd03..948d8bdbbe6 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { MainThreadConfigurationShape, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol'; @@ -18,6 +18,7 @@ import { IWorkspaceFolder, WorkspaceFolder } from 'vs/platform/workspace/common/ import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { NullLogService } from 'vs/platform/log/common/log'; import { assign } from 'vs/base/common/objects'; +import { Counter } from 'vs/base/common/numbers'; suite('ExtHostConfiguration', function () { @@ -33,7 +34,7 @@ suite('ExtHostConfiguration', function () { if (!shape) { shape = new class extends mock() { }; } - return new ExtHostConfiguration(shape, new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService()), createConfigurationData(contents)); + return new ExtHostConfiguration(shape, new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService(), new Counter()), createConfigurationData(contents)); } function createConfigurationData(contents: any): IConfigurationInitData { @@ -267,7 +268,7 @@ suite('ExtHostConfiguration', function () { test('inspect in no workspace context', function () { const testObject = new ExtHostConfiguration( new class extends mock() { }, - new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService()), + new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService(), new Counter()), { defaults: new ConfigurationModel({ 'editor': { @@ -314,7 +315,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [aWorkspaceFolder(URI.file('foo'), 0)], 'name': 'foo' - }, new NullLogService()), + }, new NullLogService(), new Counter()), { defaults: new ConfigurationModel({ 'editor': { @@ -388,7 +389,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [aWorkspaceFolder(firstRoot, 0), aWorkspaceFolder(secondRoot, 1)], 'name': 'foo' - }, new NullLogService()), + }, new NullLogService(), new Counter()), { defaults: new ConfigurationModel({ 'editor': { @@ -597,7 +598,7 @@ suite('ExtHostConfiguration', function () { 'id': 'foo', 'folders': [workspaceFolder], 'name': 'foo' - }, new NullLogService()), + }, new NullLogService(), new Counter()), createConfigurationData({ 'farboo': { 'config': false, diff --git a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts index 0b454064c07..5440265b2cc 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts @@ -6,10 +6,10 @@ 'use strict'; import * as assert from 'assert'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { DiagnosticCollection } from 'vs/workbench/api/node/extHostDiagnostics'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/node/extHostTypes'; -import { MainThreadDiagnosticsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { MainThreadDiagnosticsShape, IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { Emitter, toPromise } from 'vs/base/common/event'; @@ -27,7 +27,7 @@ suite('ExtHostDiagnostics', () => { test('disposeCheck', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.dispose(); collection.dispose(); // that's OK @@ -44,13 +44,13 @@ suite('ExtHostDiagnostics', () => { test('diagnostic collection, forEach, clear, has', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); assert.equal(collection.name, 'test'); collection.dispose(); assert.throws(() => collection.name); let c = 0; - collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.forEach(() => c++); assert.equal(c, 0); @@ -87,7 +87,7 @@ suite('ExtHostDiagnostics', () => { }); test('diagnostic collection, immutable read', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); collection.set(URI.parse('foo:bar'), [ new Diagnostic(new Range(0, 0, 1, 1), 'message-1'), new Diagnostic(new Range(0, 0, 1, 1), 'message-2') @@ -112,7 +112,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set with dupliclated tuples', function () { - let collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); let uri = URI.parse('sc:hightower'); collection.set([ [uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]], @@ -163,7 +163,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set tuple overrides, #11547', function () { let lastEntries: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 100, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('test', 'test', 100, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -190,9 +190,34 @@ suite('ExtHostDiagnostics', () => { lastEntries = undefined; }); + test('don\'t send message when not making a change', function () { + + let changeCount = 0; + let eventCount = 0; + + const emitter = new Emitter(); + emitter.event(_ => eventCount += 1); + const collection = new DiagnosticCollection('test', 'test', 100, new class extends DiagnosticsShape { + $changeMany() { + changeCount += 1; + } + }, emitter); + + let uri = URI.parse('sc:hightower'); + let diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); + + collection.set(uri, [diag]); + assert.equal(changeCount, 1); + assert.equal(eventCount, 1); + + collection.set(uri, [diag]); + assert.equal(changeCount, 1); + assert.equal(eventCount, 2); + }); + test('diagnostics collection, tuples and undefined (small array), #15585', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); let uri = URI.parse('sc:hightower'); let uri2 = URI.parse('sc:nomad'); let diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); @@ -213,7 +238,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (large array), #15585', function () { - const collection = new DiagnosticCollection('test', 100, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, new DiagnosticsShape(), new Emitter()); const tuples: [URI, Diagnostic[]][] = []; for (let i = 0; i < 500; i++) { @@ -237,7 +262,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostic capping', function () { let lastEntries: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 250, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('test', 'test', 250, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -263,7 +288,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostic eventing', async function () { let emitter = new Emitter<(string | URI)[]>(); - let collection = new DiagnosticCollection('ddd', 100, new DiagnosticsShape(), emitter); + let collection = new DiagnosticCollection('ddd', 'test', 100, new DiagnosticsShape(), emitter); let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); let diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2'); @@ -301,7 +326,7 @@ suite('ExtHostDiagnostics', () => { test('vscode.languages.onDidChangeDiagnostics Does Not Provide Document URI #49582', async function () { let emitter = new Emitter<(string | URI)[]>(); - let collection = new DiagnosticCollection('ddd', 100, new DiagnosticsShape(), emitter); + let collection = new DiagnosticCollection('ddd', 'test', 100, new DiagnosticsShape(), emitter); let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); @@ -324,7 +349,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics with related information', function (done) { - let collection = new DiagnosticCollection('ddd', 100, new class extends DiagnosticsShape { + let collection = new DiagnosticCollection('ddd', 'test', 100, new class extends DiagnosticsShape { $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { let [[, data]] = entries; @@ -347,4 +372,33 @@ suite('ExtHostDiagnostics', () => { collection.set(URI.parse('aa:bb'), [diag]); }); + + test('vscode.languages.getDiagnostics appears to return old diagnostics in some circumstances #54359', function () { + const ownerHistory: string[] = []; + const diags = new ExtHostDiagnostics(new class implements IMainContext { + getProxy(id: any): any { + return new class DiagnosticsShape { + $clear(owner: string): void { + ownerHistory.push(owner); + } + }; + } + set(): any { + return null; + } + assertRegistered(): void { + + } + }); + + let collection1 = diags.createDiagnosticCollection('foo'); + let collection2 = diags.createDiagnosticCollection('foo'); // warns, uses a different owner + + collection1.clear(); + collection2.clear(); + + assert.equal(ownerHistory.length, 2); + assert.equal(ownerHistory[0], 'foo'); + assert.equal(ownerHistory[1], 'foo0'); + }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts index daad9c035e9..3b06b1cd7d1 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; import { Position } from 'vs/workbench/api/node/extHostTypes'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index a4d4ceb1822..75a38895b19 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts index aab5a265951..878c4c8e09a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; suite('ExtHostDocumentsAndEditors', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts b/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts index 1bea8cfccef..2c1263203ea 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts @@ -6,18 +6,25 @@ import * as assert from 'assert'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/node/extHostFileSystemEventService'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; suite('ExtHostFileSystemEventService', () => { test('FileSystemWatcher ignore events properties are reversed #26851', function () { - const watcher1 = new ExtHostFileSystemEventService().createFileSystemWatcher('**/somethingInteresting', false, false, false); + const protocol: IMainContext = { + getProxy: () => { return undefined; }, + set: undefined, + assertRegistered: undefined + }; + + const watcher1 = new ExtHostFileSystemEventService(protocol, undefined).createFileSystemWatcher('**/somethingInteresting', false, false, false); assert.equal(watcher1.ignoreChangeEvents, false); assert.equal(watcher1.ignoreCreateEvents, false); assert.equal(watcher1.ignoreDeleteEvents, false); - const watcher2 = new ExtHostFileSystemEventService().createFileSystemWatcher('**/somethingBoring', true, true, true); + const watcher2 = new ExtHostFileSystemEventService(protocol, undefined).createFileSystemWatcher('**/somethingBoring', true, true, true); assert.equal(watcher2.ignoreChangeEvents, true); assert.equal(watcher2.ignoreCreateEvents, true); assert.equal(watcher2.ignoreDeleteEvents, true); diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 69c4b023ee8..18606175be9 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/node/extHostTypes'; import { TextModel as EditorModel } from 'vs/editor/common/model/textModel'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; @@ -24,7 +24,7 @@ import { IHeapService } from 'vs/workbench/api/electron-browser/mainThreadHeapSe import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { DocumentSymbolProviderRegistry, DocumentHighlightKind, Hover, ResourceTextEdit } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/codelens'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from 'vs/editor/contrib/goToDefinition/goToDefinition'; import { getHover } from 'vs/editor/contrib/hover/getHover'; @@ -37,7 +37,6 @@ import { provideSignatureHelp } from 'vs/editor/contrib/parameterHints/provideSi import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest'; import { getDocumentFormattingEdits, getDocumentRangeFormattingEdits, getOnTypeFormattingEdits } from 'vs/editor/contrib/format/format'; import { getLinks } from 'vs/editor/contrib/links/getLinks'; -import { asWinJsPromise } from 'vs/base/common/async'; import { MainContext, ExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; @@ -46,6 +45,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; import { getColors } from 'vs/editor/contrib/colorPicker/color'; +import { CancellationToken } from 'vs/base/common/cancellation'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -111,7 +111,7 @@ suite('ExtHostLanguageFeatures', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, heapService, diagnostics, new NullLogService()); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); @@ -133,7 +133,7 @@ suite('ExtHostLanguageFeatures', function () { // --- outline test('DocumentSymbols, register/deregister', function () { - assert.equal(DocumentSymbolProviderRegistry.all(model).length, 0); + assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 0); let d1 = extHost.registerDocumentSymbolProvider(defaultSelector, { provideDocumentSymbols() { return []; @@ -141,7 +141,7 @@ suite('ExtHostLanguageFeatures', function () { }); return rpcProtocol.sync().then(() => { - assert.equal(DocumentSymbolProviderRegistry.all(model).length, 1); + assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 1); d1.dispose(); return rpcProtocol.sync(); }); @@ -162,8 +162,8 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDocumentSymbols(model).then(value => { - assert.equal(value.entries.length, 1); + return getDocumentSymbols(model, true, CancellationToken.None).then(value => { + assert.equal(value.length, 1); }); }); }); @@ -177,12 +177,12 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDocumentSymbols(model).then(value => { - assert.equal(value.entries.length, 1); + return getDocumentSymbols(model, true, CancellationToken.None).then(value => { + assert.equal(value.length, 1); - let entry = value.entries[0]; + let entry = value[0]; assert.equal(entry.name, 'test'); - assert.deepEqual(entry.location.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); + assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); }); }); }); @@ -203,7 +203,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); }); }); @@ -224,13 +224,10 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let data = value[0]; - - return asWinJsPromise((token) => { - return data.provider.resolveCodeLens(model, data.symbol, token); - }).then(symbol => { + return Promise.resolve(data.provider.resolveCodeLens(model, data.symbol, CancellationToken.None)).then(symbol => { assert.equal(symbol.command.id, 'id'); assert.equal(symbol.command.title, 'Title'); }); @@ -248,13 +245,11 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getCodeLensData(model).then(value => { + return getCodeLensData(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let data = value[0]; - return asWinJsPromise((token) => { - return data.provider.resolveCodeLens(model, data.symbol, token); - }).then(symbol => { + return Promise.resolve(data.provider.resolveCodeLens(model, data.symbol, CancellationToken.None)).then(symbol => { assert.equal(symbol.command.id, 'missing'); assert.equal(symbol.command.title, '<>'); @@ -275,7 +270,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDefinitionsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getDefinitionsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -299,7 +294,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDefinitionsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getDefinitionsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 2); }); }); @@ -321,7 +316,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDefinitionsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getDefinitionsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 2); // let [first, second] = value; @@ -346,7 +341,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getDefinitionsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getDefinitionsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); }); @@ -363,7 +358,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getImplementationsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getImplementationsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -383,7 +378,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getTypeDefinitionsAtPosition(model, new EditorPosition(1, 1)).then(value => { + return getTypeDefinitionsAtPosition(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); @@ -403,7 +398,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -422,7 +417,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 4, startColumn: 1, endLineNumber: 9, endColumn: 8 }); @@ -446,9 +441,9 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getHover(model, new EditorPosition(1, 1)).then(value => { + return getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 2); - let [first, second] = value as Hover[]; + let [first, second] = value as modes.Hover[]; assert.equal(first.contents[0].value, 'registered second'); assert.equal(second.contents[0].value, 'registered first'); }); @@ -471,7 +466,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - getHover(model, new EditorPosition(1, 1)).then(value => { + getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); @@ -490,11 +485,11 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -514,11 +509,11 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -538,11 +533,11 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 3 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -563,7 +558,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return getOccurrencesAtPosition(model, new EditorPosition(1, 2)).then(value => { + return getOccurrencesAtPosition(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); }); @@ -587,7 +582,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 2); let [first, second] = value; @@ -607,7 +602,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); let [item] = value; @@ -633,7 +628,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideReferences(model, new EditorPosition(1, 2)).then(value => { + return provideReferences(model, new EditorPosition(1, 2), CancellationToken.None).then(value => { assert.equal(value.length, 1); }); @@ -841,8 +836,10 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { return rename(model, new EditorPosition(1, 1), 'newName').then(value => { - assert.equal(value.edits.length, 1); // least relevant renamer - assert.equal((value.edits)[0].edits.length, 2); // least relevant renamer + // least relevant rename provider + assert.equal(value.edits.length, 2); + assert.equal((value.edits[0]).edits.length, 1); + assert.equal((value.edits[1]).edits.length, 1); }); }); }); @@ -869,7 +866,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1)).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => { assert.ok(value); }); }); @@ -884,7 +881,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1)).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => { assert.equal(value, undefined); }); }); @@ -1008,7 +1005,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }).then(value => { + return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None).then(value => { assert.equal(value.length, 2); let [first, second] = value; assert.equal(first.text, 'testing'); @@ -1029,7 +1026,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }); + return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None); }); }); @@ -1054,7 +1051,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }).then(value => { + return getDocumentFormattingEdits(model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; assert.equal(first.text, 'testing'); @@ -1071,7 +1068,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }).then(value => { + return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; assert.equal(first.text, 'testing'); @@ -1097,7 +1094,7 @@ suite('ExtHostLanguageFeatures', function () { } })); return rpcProtocol.sync().then(() => { - return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }).then(value => { + return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; assert.equal(first.text, 'range2'); @@ -1117,7 +1114,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }); + return getDocumentRangeFormattingEdits(model, new EditorRange(1, 1, 1, 1), { insertSpaces: true, tabSize: 4 }, CancellationToken.None); }); }); @@ -1149,7 +1146,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getLinks(model).then(value => { + return getLinks(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; @@ -1174,7 +1171,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getLinks(model).then(value => { + return getLinks(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; @@ -1196,7 +1193,7 @@ suite('ExtHostLanguageFeatures', function () { })); return rpcProtocol.sync().then(() => { - return getColors(model).then(value => { + return getColors(model, CancellationToken.None).then(value => { assert.equal(value.length, 1); let [first] = value; diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index b1b4ee95411..138d41fa2f7 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -5,11 +5,10 @@ 'use strict'; import * as assert from 'assert'; -import * as path from 'path'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; -import URI, { UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import * as extfs from 'vs/base/node/extfs'; import { IFileMatch, IPatternInfo, IRawFileMatch2, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, QueryType } from 'vs/platform/search/common/search'; @@ -18,6 +17,7 @@ import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; import { Range } from 'vs/workbench/api/node/extHostTypes'; import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import * as vscode from 'vscode'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; let rpcProtocol: TestRPCProtocol; let extHostSearch: ExtHostSearch; @@ -29,19 +29,27 @@ class MockMainThreadSearch implements MainThreadSearchShape { results: (UriComponents | IRawFileMatch2)[] = []; - $registerSearchProvider(handle: number, scheme: string): void { + $registerFileSearchProvider(handle: number, scheme: string): void { + this.lastHandle = handle; + } + + $registerFileIndexProvider(handle: number, scheme: string): void { + this.lastHandle = handle; + } + + $registerTextSearchProvider(handle: number, scheme: string): void { this.lastHandle = handle; } $unregisterProvider(handle: number): void { } - $handleFindMatch(handle: number, session: number, data: UriComponents | IRawFileMatch2[]): void { - if (Array.isArray(data)) { - this.results.push(...data); - } else { - this.results.push(data); - } + $handleFileMatch(handle: number, session: number, data: UriComponents[]): void { + this.results.push(...data); + } + + $handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void { + this.results.push(...data); } $handleTelemetry(eventName: string, data: any): void { @@ -54,18 +62,24 @@ class MockMainThreadSearch implements MainThreadSearchShape { let mockExtfs: Partial; suite('ExtHostSearch', () => { - async function registerTestSearchProvider(provider: vscode.SearchProvider, scheme = 'file'): TPromise { - disposables.push(extHostSearch.registerSearchProvider(scheme, provider)); + async function registerTestTextSearchProvider(provider: vscode.TextSearchProvider, scheme = 'file'): Promise { + disposables.push(extHostSearch.registerTextSearchProvider(scheme, provider)); await rpcProtocol.sync(); } - async function runFileSearch(query: IRawSearchQuery, cancel = false): TPromise<{ results: URI[]; stats: ISearchCompleteStats }> { + async function registerTestFileSearchProvider(provider: vscode.FileSearchProvider, scheme = 'file'): Promise { + disposables.push(extHostSearch.registerFileSearchProvider(scheme, provider)); + await rpcProtocol.sync(); + } + + async function runFileSearch(query: IRawSearchQuery, cancel = false): Promise<{ results: URI[]; stats: ISearchCompleteStats }> { let stats: ISearchCompleteStats; try { - const p = extHostSearch.$provideFileSearchResults(mockMainThreadSearch.lastHandle, 0, query); + const cancellation = new CancellationTokenSource(); + const p = extHostSearch.$provideFileSearchResults(mockMainThreadSearch.lastHandle, 0, query, cancellation.token); if (cancel) { await new TPromise(resolve => process.nextTick(resolve)); - p.cancel(); + cancellation.cancel(); } stats = await p; @@ -83,13 +97,14 @@ suite('ExtHostSearch', () => { }; } - async function runTextSearch(pattern: IPatternInfo, query: IRawSearchQuery, cancel = false): TPromise<{ results: IFileMatch[], stats: ISearchCompleteStats }> { + async function runTextSearch(pattern: IPatternInfo, query: IRawSearchQuery, cancel = false): Promise<{ results: IFileMatch[], stats: ISearchCompleteStats }> { let stats: ISearchCompleteStats; try { - const p = extHostSearch.$provideTextSearchResults(mockMainThreadSearch.lastHandle, 0, pattern, query); + const cancellation = new CancellationTokenSource(); + const p = extHostSearch.$provideTextSearchResults(mockMainThreadSearch.lastHandle, 0, pattern, query, cancellation.token); if (cancel) { await new TPromise(resolve => process.nextTick(resolve)); - p.cancel(); + cancellation.cancel(); } stats = await p; @@ -154,8 +169,8 @@ suite('ExtHostSearch', () => { } test('no results', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { return TPromise.wrap(null); } }); @@ -172,10 +187,9 @@ suite('ExtHostSearch', () => { joinPath(rootFolderA, 'file3.ts') ]; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); - return TPromise.wrap(null); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { + return TPromise.wrap(reportedResults); } }); @@ -187,14 +201,13 @@ suite('ExtHostSearch', () => { test('Search canceled', async () => { let cancelRequested = false; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { return new TPromise((resolve, reject) => { token.onCancellationRequested(() => { cancelRequested = true; - progress.report('file1.ts'); - resolve(null); // or reject or nothing? + resolve([joinPath(options.folder, 'file1.ts')]); // or reject or nothing? }); }); } @@ -205,31 +218,9 @@ suite('ExtHostSearch', () => { assert(!results.length); }); - test('provider fail', async () => { - const reportedResults = [ - 'file1.ts', - 'file2.ts', - 'file3.ts', - ]; - - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(r)); - throw new Error('I broke'); - } - }); - - try { - await runFileSearch(getSimpleQuery()); - assert(false, 'Expected to fail'); - } catch { - // Expected to throw - } - }); - test('provider returns null', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { return null; } }); @@ -243,8 +234,8 @@ suite('ExtHostSearch', () => { }); test('all provider calls get global include/excludes', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { assert(options.excludes.length === 2 && options.includes.length === 2, 'Missing global include/excludes'); return TPromise.wrap(null); } @@ -272,8 +263,8 @@ suite('ExtHostSearch', () => { }); test('global/local include/excludes combined', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { if (options.folder.toString() === rootFolderA.toString()) { assert.deepEqual(options.includes.sort(), ['*.ts', 'foo']); assert.deepEqual(options.excludes.sort(), ['*.js', 'bar']); @@ -314,8 +305,8 @@ suite('ExtHostSearch', () => { }); test('include/excludes resolved correctly', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { assert.deepEqual(options.includes.sort(), ['*.jsx', '*.ts']); assert.deepEqual(options.excludes.sort(), []); @@ -357,10 +348,10 @@ suite('ExtHostSearch', () => { 'file1.js', ]; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(r)); - return TPromise.wrap(null); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { + return TPromise.wrap(reportedResults + .map(relativePath => joinPath(options.folder, relativePath))); } }); @@ -388,25 +379,24 @@ suite('ExtHostSearch', () => { test('multiroot sibling exclude clause', async () => { - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - let reportedResults; + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { + let reportedResults: URI[]; if (options.folder.fsPath === rootFolderA.fsPath) { reportedResults = [ 'folder/fileA.scss', 'folder/fileA.css', 'folder/file2.css' - ]; + ].map(relativePath => joinPath(rootFolderA, relativePath)); } else { reportedResults = [ 'fileB.ts', 'fileB.js', 'file3.js' - ]; + ].map(relativePath => joinPath(rootFolderB, relativePath)); } - reportedResults.forEach(r => progress.report(r)); - return TPromise.wrap(null); + return TPromise.wrap(reportedResults); } }); @@ -451,7 +441,7 @@ suite('ExtHostSearch', () => { ]); }); - test('max results = 1', async () => { + test.skip('max results = 1', async () => { const reportedResults = [ joinPath(rootFolderA, 'file1.ts'), joinPath(rootFolderA, 'file2.ts'), @@ -459,12 +449,11 @@ suite('ExtHostSearch', () => { ]; let wasCanceled = false; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); - return TPromise.wrap(null); + return TPromise.wrap(reportedResults); } }); @@ -488,7 +477,7 @@ suite('ExtHostSearch', () => { assert(wasCanceled, 'Expected to be canceled when hitting limit'); }); - test('max results = 2', async () => { + test.skip('max results = 2', async () => { const reportedResults = [ joinPath(rootFolderA, 'file1.ts'), joinPath(rootFolderA, 'file2.ts'), @@ -496,12 +485,11 @@ suite('ExtHostSearch', () => { ]; let wasCanceled = false; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); - return TPromise.wrap(null); + return TPromise.wrap(reportedResults); } }); @@ -525,19 +513,18 @@ suite('ExtHostSearch', () => { assert(wasCanceled, 'Expected to be canceled when hitting limit'); }); - test('provider returns maxResults exactly', async () => { + test.skip('provider returns maxResults exactly', async () => { const reportedResults = [ joinPath(rootFolderA, 'file1.ts'), joinPath(rootFolderA, 'file2.ts'), ]; let wasCanceled = false; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); - return TPromise.wrap(null); + return TPromise.wrap(reportedResults); } }); @@ -563,20 +550,18 @@ suite('ExtHostSearch', () => { test('multiroot max results', async () => { let cancels = 0; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => cancels++); // Provice results async so it has a chance to invoke every provider return new TPromise(r => process.nextTick(r)) .then(() => { - [ + return [ 'file1.ts', 'file2.ts', 'file3.ts', - ].forEach(f => { - progress.report(f); - }); + ].map(relativePath => joinPath(options.folder, relativePath)); }); } }); @@ -602,37 +587,6 @@ suite('ExtHostSearch', () => { assert.equal(cancels, 2, 'Expected all invocations to be canceled when hitting limit'); }); - test('respects filePattern', async () => { - const reportedResults = [ - joinPath(rootFolderA, 'file1.ts'), - joinPath(rootFolderA, 'file2.ts'), - joinPath(rootFolderA, 'file3.ts'), - ]; - - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); - return TPromise.wrap(null); - } - }); - - const query: ISearchQuery = { - type: QueryType.File, - - filePattern: 'file3', - - folderQueries: [ - { - folder: rootFolderA - } - ] - }; - - const { results } = await runFileSearch(query); - assert.equal(results.length, 1); - compareURIs(results, reportedResults.slice(2)); - }); - test('works with non-file schemes', async () => { const reportedResults = [ joinPath(fancySchemeFolderA, 'file1.ts'), @@ -641,10 +595,9 @@ suite('ExtHostSearch', () => { ]; - await registerTestSearchProvider({ - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); - return TPromise.wrap(null); + await registerTestFileSearchProvider({ + provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { + return TPromise.wrap(reportedResults); } }, fancyScheme); @@ -661,28 +614,6 @@ suite('ExtHostSearch', () => { const { results } = await runFileSearch(query); compareURIs(results, reportedResults); }); - - // Mock fs? - // test('Returns result for absolute path', async () => { - // const queriedFile = makeFileResult(rootFolderA, 'file2.ts'); - // const reportedResults = [ - // makeFileResult(rootFolderA, 'file1.ts'), - // queriedFile, - // makeFileResult(rootFolderA, 'file3.ts'), - // ]; - - // await registerTestSearchProvider({ - // provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { - // reportedResults.forEach(r => progress.report(r)); - // return TPromise.wrap(null); - // } - // }); - - // const queriedFilePath = queriedFile.fsPath; - // const { results } = await runFileSearch(getSimpleQuery(queriedFilePath)); - // assert.equal(results.length, 1); - // compareURIs(results, [queriedFile]); - // }); }); suite('Text:', () => { @@ -694,11 +625,11 @@ suite('ExtHostSearch', () => { }; } - function makeTextResult(relativePath: string): vscode.TextSearchResult { + function makeTextResult(baseFolder: URI, relativePath: string): vscode.TextSearchResult { return { preview: makePreview('foo'), range: new Range(0, 0, 0, 3), - path: relativePath + uri: joinPath(baseFolder, relativePath) }; } @@ -718,19 +649,19 @@ suite('ExtHostSearch', () => { }; } - function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[], folder = rootFolderA) { + function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) { const actualTextSearchResults: vscode.TextSearchResult[] = []; for (let fileMatch of actual) { // Make relative - const relativePath = fileMatch.resource.toString().substr(folder.toString().length + 1); - for (let lineMatch of fileMatch.lineMatches) { - for (let [offset, length] of lineMatch.offsetAndLengths) { - actualTextSearchResults.push({ - preview: { text: lineMatch.preview, match: null }, - range: new Range(lineMatch.lineNumber, offset, lineMatch.lineNumber, length + offset), - path: relativePath - }); - } + for (let lineMatch of fileMatch.matches) { + actualTextSearchResults.push({ + preview: { + text: lineMatch.preview.text, + match: new Range(lineMatch.preview.match.startLineNumber, lineMatch.preview.match.startColumn, lineMatch.preview.match.endLineNumber, lineMatch.preview.match.endColumn) + }, + range: new Range(lineMatch.range.startLineNumber, lineMatch.range.startColumn, lineMatch.range.endLineNumber, lineMatch.range.endColumn), + uri: fileMatch.resource + }); } } @@ -741,7 +672,7 @@ suite('ExtHostSearch', () => { .map(r => ({ ...r, ...{ - uri: r.path.toString(), + uri: r.uri.toString(), range: rangeToString(r.range), preview: { text: r.preview.text, @@ -756,7 +687,7 @@ suite('ExtHostSearch', () => { } test('no results', async () => { - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { return TPromise.wrap(null); } @@ -769,11 +700,11 @@ suite('ExtHostSearch', () => { test('basic results', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { providedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); @@ -786,7 +717,7 @@ suite('ExtHostSearch', () => { }); test('all provider calls get global include/excludes', async () => { - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { assert.equal(options.includes.length, 1); assert.equal(options.excludes.length, 1); @@ -815,7 +746,7 @@ suite('ExtHostSearch', () => { }); test('global/local include/excludes combined', async () => { - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { if (options.folder.toString() === rootFolderA.toString()) { assert.deepEqual(options.includes.sort(), ['*.ts', 'foo']); @@ -856,7 +787,7 @@ suite('ExtHostSearch', () => { }); test('include/excludes resolved correctly', async () => { - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { assert.deepEqual(options.includes.sort(), ['*.jsx', '*.ts']); assert.deepEqual(options.excludes.sort(), []); @@ -893,7 +824,7 @@ suite('ExtHostSearch', () => { }); test('provider fail', async () => { - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { throw new Error('Provider fail'); } @@ -920,11 +851,11 @@ suite('ExtHostSearch', () => { }; const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.js'), - makeTextResult('file1.ts') + makeTextResult(rootFolderA, 'file1.js'), + makeTextResult(rootFolderA, 'file1.ts') ]; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { providedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); @@ -968,20 +899,20 @@ suite('ExtHostSearch', () => { } }; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { let reportedResults; if (options.folder.fsPath === rootFolderA.fsPath) { reportedResults = [ - makeTextResult('folder/fileA.scss'), - makeTextResult('folder/fileA.css'), - makeTextResult('folder/file2.css') + makeTextResult(rootFolderA, 'folder/fileA.scss'), + makeTextResult(rootFolderA, 'folder/fileA.css'), + makeTextResult(rootFolderA, 'folder/file2.css') ]; } else { reportedResults = [ - makeTextResult('fileB.ts'), - makeTextResult('fileB.js'), - makeTextResult('file3.js') + makeTextResult(rootFolderB, 'fileB.ts'), + makeTextResult(rootFolderB, 'fileB.js'), + makeTextResult(rootFolderB, 'file3.js') ]; } @@ -1019,20 +950,20 @@ suite('ExtHostSearch', () => { const { results } = await runTextSearch(getPattern('foo'), query); assertResults(results, [ - makeTextResult('folder/fileA.scss'), - makeTextResult('folder/file2.css'), - makeTextResult('fileB.ts'), - makeTextResult('fileB.js'), - makeTextResult('file3.js')]); + makeTextResult(rootFolderA, 'folder/fileA.scss'), + makeTextResult(rootFolderA, 'folder/file2.css'), + makeTextResult(rootFolderB, 'fileB.ts'), + makeTextResult(rootFolderB, 'fileB.js'), + makeTextResult(rootFolderB, 'file3.js')]); }); test('include pattern applied', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.js'), - makeTextResult('file1.ts') + makeTextResult(rootFolderA, 'file1.js'), + makeTextResult(rootFolderA, 'file1.ts') ]; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { providedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); @@ -1057,12 +988,12 @@ suite('ExtHostSearch', () => { test('max results = 1', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; let wasCanceled = false; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); providedResults.forEach(r => progress.report(r)); @@ -1088,13 +1019,13 @@ suite('ExtHostSearch', () => { test('max results = 2', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts'), - makeTextResult('file3.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts'), + makeTextResult(rootFolderA, 'file3.ts') ]; let wasCanceled = false; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); providedResults.forEach(r => progress.report(r)); @@ -1120,12 +1051,12 @@ suite('ExtHostSearch', () => { test('provider returns maxResults exactly', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts') + makeTextResult(rootFolderA, 'file1.ts'), + makeTextResult(rootFolderA, 'file2.ts') ]; let wasCanceled = false; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => wasCanceled = true); providedResults.forEach(r => progress.report(r)); @@ -1151,7 +1082,7 @@ suite('ExtHostSearch', () => { test('multiroot max results', async () => { let cancels = 0; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { token.onCancellationRequested(() => cancels++); return new TPromise(r => process.nextTick(r)) @@ -1160,7 +1091,7 @@ suite('ExtHostSearch', () => { 'file1.ts', 'file2.ts', 'file3.ts', - ].forEach(f => progress.report(makeTextResult(f))); + ].forEach(f => progress.report(makeTextResult(options.folder, f))); }); } }); @@ -1183,12 +1114,12 @@ suite('ExtHostSearch', () => { test('works with non-file schemes', async () => { const providedResults: vscode.TextSearchResult[] = [ - makeTextResult('file1.ts'), - makeTextResult('file2.ts'), - makeTextResult('file3.ts') + makeTextResult(fancySchemeFolderA, 'file1.ts'), + makeTextResult(fancySchemeFolderA, 'file2.ts'), + makeTextResult(fancySchemeFolderA, 'file3.ts') ]; - await registerTestSearchProvider({ + await registerTestTextSearchProvider({ provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { providedResults.forEach(r => progress.report(r)); return TPromise.wrap(null); @@ -1204,7 +1135,7 @@ suite('ExtHostSearch', () => { }; const { results } = await runTextSearch(getPattern('foo'), query); - assertResults(results, providedResults, fancySchemeFolderA); + assertResults(results, providedResults); }); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts index ff187f60156..2b75c942682 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts @@ -6,21 +6,22 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import { TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; +import { TextEditorLineNumbersStyle, Range } from 'vs/workbench/api/node/extHostTypes'; import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { MainThreadTextEditorsShape, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostTextEditorOptions, ExtHostTextEditor } from 'vs/workbench/api/node/extHostTextEditor'; import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/workbench/test/electron-browser/api/mock'; suite('ExtHostTextEditor', () => { let editor: ExtHostTextEditor; + let doc = new ExtHostDocumentData(undefined, URI.file(''), [ + 'aaaa bbbb+cccc abc' + ], '\n', 'text', 1, false); setup(() => { - let doc = new ExtHostDocumentData(undefined, URI.file(''), [ - 'aaaa bbbb+cccc abc' - ], '\n', 'text', 1, false); editor = new ExtHostTextEditor(null, 'fake', doc, [], { cursorStyle: 0, insertSpaces: true, lineNumbers: 1, tabSize: 4 }, [], 1); }); @@ -39,6 +40,25 @@ suite('ExtHostTextEditor', () => { assert.throws(() => editor._acceptOptions(null)); assert.throws(() => editor._acceptSelections([])); }); + + test('API [bug]: registerTextEditorCommand clears redo stack even if no edits are made #55163', async function () { + let applyCount = 0; + let editor = new ExtHostTextEditor(new class extends mock() { + $tryApplyEdits(): TPromise { + applyCount += 1; + return TPromise.wrap(true); + } + }, 'edt1', doc, [], { cursorStyle: 0, insertSpaces: true, lineNumbers: 1, tabSize: 4 }, [], 1); + + await editor.edit(edit => { }); + assert.equal(applyCount, 0); + + await editor.edit(edit => { edit.setEndOfLine(1); }); + assert.equal(applyCount, 1); + + await editor.edit(edit => { edit.delete(new Range(0, 0, 1, 1)); }); + assert.equal(applyCount, 2); + }); }); suite('ExtHostTextEditorOptions', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts index 8f6d051243b..d263621ce46 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; import { MainContext, MainThreadTextEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index 284f16ac8ee..e4b5a3f176a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -72,7 +72,7 @@ suite('ExtHostTreeView', function () { rpcProtocol.set(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, rpcProtocol)); target = new RecordingShape(); - testObject = new ExtHostTreeViews(target, new ExtHostCommands(rpcProtocol, new ExtHostHeapService(), new NullLogService())); + testObject = new ExtHostTreeViews(target, new ExtHostCommands(rpcProtocol, new ExtHostHeapService(), new NullLogService()), new NullLogService()); onDidChangeTreeNode = new Emitter<{ key: string }>(); onDidChangeTreeNodeWithId = new Emitter<{ key: string }>(); testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }); @@ -416,7 +416,7 @@ suite('ExtHostTreeView', function () { assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); assert.deepEqual({ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }, removeUnsetKeys(revealTarget.args[0][1])); assert.deepEqual([], revealTarget.args[0][2]); - assert.equal(void 0, revealTarget.args[0][3]); + assert.deepEqual({ select: true, focus: false }, revealTarget.args[0][3]); }); }); @@ -429,7 +429,7 @@ suite('ExtHostTreeView', function () { assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); assert.deepEqual({ handle: '0/0:a/0:aa', label: 'aa', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:a' }, removeUnsetKeys(revealTarget.args[0][1])); assert.deepEqual([{ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.equal(void 0, revealTarget.args[0][3]); + assert.deepEqual({ select: true, focus: false }, revealTarget.args[0][3]); }); }); @@ -444,7 +444,7 @@ suite('ExtHostTreeView', function () { assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); assert.deepEqual({ handle: '0/0:a/0:aa', label: 'aa', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:a' }, removeUnsetKeys(revealTarget.args[0][1])); assert.deepEqual([{ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.equal(void 0, revealTarget.args[0][3]); + assert.deepEqual({ select: true, focus: false }, revealTarget.args[0][3]); })); }); @@ -458,7 +458,7 @@ suite('ExtHostTreeView', function () { }; const revealTarget = sinon.spy(target, '$reveal'); const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aCompleteNodeTreeDataProvider() }); - return treeView.reveal({ key: 'bac' }, { select: false }) + return treeView.reveal({ key: 'bac' }, { select: false, focus: false }) .then(() => { assert.ok(revealTarget.calledOnce); assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); @@ -467,7 +467,7 @@ suite('ExtHostTreeView', function () { { handle: '0/0:b', label: 'b', collapsibleState: TreeItemCollapsibleState.Collapsed }, { handle: '0/0:b/0:ba', label: 'ba', collapsibleState: TreeItemCollapsibleState.Collapsed, parentHandle: '0/0:b' } ], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.deepEqual({ select: false }, revealTarget.args[0][3]); + assert.deepEqual({ select: false, focus: false }, revealTarget.args[0][3]); }); }); @@ -494,7 +494,7 @@ suite('ExtHostTreeView', function () { assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); assert.deepEqual({ handle: '0/0:a/0:ac', label: 'ac', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:a' }, removeUnsetKeys(revealTarget.args[0][1])); assert.deepEqual([{ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.equal(void 0, revealTarget.args[0][3]); + assert.deepEqual({ select: true, focus: false }, revealTarget.args[0][3]); }); }); }); @@ -533,12 +533,12 @@ suite('ExtHostTreeView', function () { assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); assert.deepEqual({ handle: '0/0:b/0:bc', label: 'bc', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b' }, removeUnsetKeys(revealTarget.args[0][1])); assert.deepEqual([{ handle: '0/0:b', label: 'b', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); - assert.equal(void 0, revealTarget.args[0][3]); + assert.deepEqual({ select: true, focus: false }, revealTarget.args[0][3]); }); }); }); - function loadCompleteTree(treeId, element?: string): TPromise { + function loadCompleteTree(treeId, element?: string) { return testObject.$getChildren(treeId, element) .then(elements => elements.map(e => loadCompleteTree(treeId, e.handle))) .then(() => null); @@ -637,4 +637,4 @@ suite('ExtHostTreeView', function () { constructor(readonly key: string) { } } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts index 316a1e3a3cc..00544e0a62e 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/node/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; @@ -368,60 +368,61 @@ suite('ExtHostTypes', function () { ]); edit.set(b, undefined); - assert.ok(edit.has(b)); - assert.equal(edit.size, 2); + assert.ok(!edit.has(b)); + assert.equal(edit.size, 1); edit.set(b, [types.TextEdit.insert(new types.Position(0, 0), 'ffff')]); assert.equal(edit.get(b).length, 1); - }); - // test('WorkspaceEdit should fail when editing deleted resource', () => { - // const resource = URI.parse('file:///a.ts'); + test('WorkspaceEdit - keep order of text and file changes', function () { - // const edit = new types.WorkspaceEdit(); - // edit.deleteResource(resource); - // try { - // edit.insert(resource, new types.Position(0, 0), ''); - // assert.fail(false, 'Should disallow edit of deleted resource'); - // } catch { - // // expected - // } - // }); + const edit = new types.WorkspaceEdit(); + edit.replace(URI.parse('foo:a'), new types.Range(1, 1, 1, 1), 'foo'); + edit.renameFile(URI.parse('foo:a'), URI.parse('foo:b')); + edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); + edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); - // test('WorkspaceEdit - keep order of text and file changes', function () { + const all = edit._allEntries(); + assert.equal(all.length, 4); - // const edit = new types.WorkspaceEdit(); - // edit.replace(URI.parse('foo:a'), new types.Range(1, 1, 1, 1), 'foo'); - // edit.renameResource(URI.parse('foo:a'), URI.parse('foo:b')); - // edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); - // edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); + function isFileChange(thing: [URI, types.TextEdit[]] | [URI, URI, { overwrite?: boolean }]): thing is [URI, URI, { overwrite?: boolean }] { + const [f, s] = thing; + return URI.isUri(f) && URI.isUri(s); + } - // const all = edit.allEntries(); - // assert.equal(all.length, 3); + function isTextChange(thing: [URI, types.TextEdit[]] | [URI, URI, { overwrite?: boolean }]): thing is [URI, types.TextEdit[]] { + const [f, s] = thing; + return URI.isUri(f) && Array.isArray(s); + } - // function isFileChange(thing: [URI, types.TextEdit[]] | [URI, URI]): thing is [URI, URI] { - // const [f, s] = thing; - // return URI.isUri(f) && URI.isUri(s); - // } + const [first, second, third, fourth] = all; + assert.equal(first[0].toString(), 'foo:a'); + assert.ok(!isFileChange(first)); + assert.ok(isTextChange(first) && first[1].length === 1); - // function isTextChange(thing: [URI, types.TextEdit[]] | [URI, URI]): thing is [URI, types.TextEdit[]] { - // const [f, s] = thing; - // return URI.isUri(f) && Array.isArray(s); - // } + assert.equal(second[0].toString(), 'foo:a'); + assert.ok(isFileChange(second)); - // const [first, second, third] = all; - // assert.equal(first[0].toString(), 'foo:a'); - // assert.ok(!isFileChange(first)); - // assert.ok(isTextChange(first) && first[1].length === 2); + assert.equal(third[0].toString(), 'foo:a'); + assert.ok(isTextChange(third) && third[1].length === 1); - // assert.equal(second[0].toString(), 'foo:a'); - // assert.ok(isFileChange(second)); + assert.equal(fourth[0].toString(), 'foo:b'); + assert.ok(!isFileChange(fourth)); + assert.ok(isTextChange(fourth) && fourth[1].length === 1); + }); - // assert.equal(third[0].toString(), 'foo:b'); - // assert.ok(!isFileChange(third)); - // assert.ok(isTextChange(third) && third[1].length === 1); - // }); + test('WorkspaceEdit - two edits for one resource', function () { + let edit = new types.WorkspaceEdit(); + let uri = URI.parse('foo:bar'); + edit.insert(uri, new types.Position(0, 0), 'Hello'); + edit.insert(uri, new types.Position(0, 0), 'Foo'); + + assert.equal(edit._allEntries().length, 2); + let [first, second] = edit._allEntries(); + assert.equal((first as [URI, types.TextEdit[]])[1][0].newText, 'Hello'); + assert.equal((second as [URI, types.TextEdit[]])[1][0].newText, 'Foo'); + }); test('DocumentLink', function () { assert.throws(() => new types.DocumentLink(null, null)); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index 5b03bd47a60..052def1fa34 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { basename } from 'path'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { TestRPCProtocol } from './testRPCProtocol'; @@ -15,6 +15,7 @@ import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { Counter } from 'vs/base/common/numbers'; suite('ExtHostWorkspace', function () { @@ -41,7 +42,7 @@ suite('ExtHostWorkspace', function () { test('asRelativePath', function () { - const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/Applications/NewsWoWBot'), 0)], name: 'Test' }, new NullLogService()); + const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/Applications/NewsWoWBot'), 0)], name: 'Test' }, new NullLogService(), new Counter()); assertAsRelativePath(ws, '/Coding/Applications/NewsWoWBot/bernd/das/brot', 'bernd/das/brot'); assertAsRelativePath(ws, '/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart', @@ -55,7 +56,7 @@ suite('ExtHostWorkspace', function () { test('asRelativePath, same paths, #11402', function () { const root = '/home/aeschli/workspaces/samples/docker'; const input = '/home/aeschli/workspaces/samples/docker'; - const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file(root), 0)], name: 'Test' }, new NullLogService()); + const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file(root), 0)], name: 'Test' }, new NullLogService(), new Counter()); assertAsRelativePath(ws, (input), input); @@ -64,20 +65,20 @@ suite('ExtHostWorkspace', function () { }); test('asRelativePath, no workspace', function () { - const ws = new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService()); + const ws = new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService(), new Counter()); assertAsRelativePath(ws, (''), ''); assertAsRelativePath(ws, ('/foo/bar'), '/foo/bar'); }); test('asRelativePath, multiple folders', function () { - const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService()); + const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService(), new Counter()); assertAsRelativePath(ws, '/Coding/One/file.txt', 'One/file.txt'); assertAsRelativePath(ws, '/Coding/Two/files/out.txt', 'Two/files/out.txt'); assertAsRelativePath(ws, '/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt'); }); test('slightly inconsistent behaviour of asRelativePath and getWorkspaceFolder, #31553', function () { - const mrws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService()); + const mrws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService(), new Counter()); assertAsRelativePath(mrws, '/Coding/One/file.txt', 'One/file.txt'); assertAsRelativePath(mrws, '/Coding/One/file.txt', 'One/file.txt', true); @@ -89,7 +90,7 @@ suite('ExtHostWorkspace', function () { assertAsRelativePath(mrws, '/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', true); assertAsRelativePath(mrws, '/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', false); - const srws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0)], name: 'Test' }, new NullLogService()); + const srws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0)], name: 'Test' }, new NullLogService(), new Counter()); assertAsRelativePath(srws, '/Coding/One/file.txt', 'file.txt'); assertAsRelativePath(srws, '/Coding/One/file.txt', 'file.txt', false); assertAsRelativePath(srws, '/Coding/One/file.txt', 'One/file.txt', true); @@ -99,24 +100,24 @@ suite('ExtHostWorkspace', function () { }); test('getPath, legacy', function () { - let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService(), new Counter()); assert.equal(ws.getPath(), undefined); - ws = new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService()); + ws = new ExtHostWorkspace(new TestRPCProtocol(), null, new NullLogService(), new Counter()); assert.equal(ws.getPath(), undefined); - ws = new ExtHostWorkspace(new TestRPCProtocol(), undefined, new NullLogService()); + ws = new ExtHostWorkspace(new TestRPCProtocol(), undefined, new NullLogService(), new Counter()); assert.equal(ws.getPath(), undefined); - ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('Folder'), 0), aWorkspaceFolderData(URI.file('Another/Folder'), 1)] }, new NullLogService()); + ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('Folder'), 0), aWorkspaceFolderData(URI.file('Another/Folder'), 1)] }, new NullLogService(), new Counter()); assert.equal(ws.getPath().replace(/\\/g, '/'), '/Folder'); - ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('/Folder'), 0)] }, new NullLogService()); + ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('/Folder'), 0)] }, new NullLogService(), new Counter()); assert.equal(ws.getPath().replace(/\\/g, '/'), '/Folder'); }); test('WorkspaceFolder has name and index', function () { - const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService()); + const ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', folders: [aWorkspaceFolderData(URI.file('/Coding/One'), 0), aWorkspaceFolderData(URI.file('/Coding/Two'), 1)], name: 'Test' }, new NullLogService(), new Counter()); const [one, two] = ws.getWorkspaceFolders(); @@ -135,7 +136,7 @@ suite('ExtHostWorkspace', function () { aWorkspaceFolderData(URI.file('/Coding/Two'), 1), aWorkspaceFolderData(URI.file('/Coding/Two/Nested'), 2) ] - }, new NullLogService()); + }, new NullLogService(), new Counter()); let folder = ws.getWorkspaceFolder(URI.file('/foo/bar')); assert.equal(folder, undefined); @@ -175,7 +176,7 @@ suite('ExtHostWorkspace', function () { }); test('Multiroot change event should have a delta, #29641', function (done) { - let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService(), new Counter()); let finished = false; const finish = (error?: any) => { @@ -238,7 +239,7 @@ suite('ExtHostWorkspace', function () { }); test('Multiroot change keeps existing workspaces live', function () { - let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService()); + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService(), new Counter()); let firstFolder = ws.getWorkspaceFolders()[0]; ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar2'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1, 'renamed')] }); @@ -258,7 +259,7 @@ suite('ExtHostWorkspace', function () { }); test('updateWorkspaceFolders - invalid arguments', function () { - let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService(), new Counter()); assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, null, null)); assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 0)); @@ -267,7 +268,7 @@ suite('ExtHostWorkspace', function () { assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, -1, 0)); assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, -1, -1)); - ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService()); + ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService(), new Counter()); assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 1, 1)); assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 2)); @@ -289,7 +290,7 @@ suite('ExtHostWorkspace', function () { assertRegistered: undefined }; - const ws = new ExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + const ws = new ExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }, new NullLogService(), new Counter()); // // Add one folder @@ -522,15 +523,15 @@ suite('ExtHostWorkspace', function () { } }; - let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService(), new Counter()); let sub = ws.onDidChangeWorkspace(e => { try { assert.throws(() => { (e).added = []; }); - assert.throws(() => { - (e.added)[0] = null; - }); + // assert.throws(() => { + // (e.added)[0] = null; + // }); } catch (error) { finish(error); } @@ -545,7 +546,7 @@ suite('ExtHostWorkspace', function () { id: 'foo', name: 'Test', folders: [ aWorkspaceFolderData(URI.file('c:/Users/marek/Desktop/vsc_test/'), 0) ] - }, new NullLogService()); + }, new NullLogService(), new Counter()); assert.ok(ws.getWorkspaceFolder(URI.file('c:/Users/marek/Desktop/vsc_test/a.txt'))); assert.ok(ws.getWorkspaceFolder(URI.file('C:/Users/marek/Desktop/vsc_test/b.txt'))); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts index 4cf96c8b3e8..bff0bac85ba 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IConfigurationRegistry, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts index a5238a9372e..9eeeb4b3492 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadDiagnostics.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { MarkerService } from 'vs/platform/markers/common/markerService'; import { MainThreadDiagnostics } from 'vs/workbench/api/electron-browser/mainThreadDiagnostics'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; suite('MainThreadDiagnostics', function () { diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts index 2057af0b39a..060ed7d2a53 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -16,16 +16,19 @@ import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { Event } from 'vs/base/common/event'; import { MainThreadTextEditors } from 'vs/workbench/api/electron-browser/mainThreadEditors'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { TestFileService, TestEditorService, TestEditorGroupsService } from 'vs/workbench/test/workbenchTestServices'; +import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFileStat } from 'vs/platform/files/common/files'; import { ResourceTextEdit } from 'vs/editor/common/modes'; import { BulkEditService } from 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IReference, ImmortalReference } from 'vs/base/common/lifecycle'; +import { LabelService } from 'vs/platform/label/common/label'; suite('MainThreadEditors', () => { @@ -47,23 +50,22 @@ suite('MainThreadEditors', () => { createdResources.clear(); deletedResources.clear(); - const fileService = new class extends TestFileService { - async moveFile(from: URI, target: URI): TPromise { - movedResources.set(from, target); - return createMockFileStat(target); - } - async createFile(uri: URI): TPromise { - createdResources.add(uri); - return createMockFileStat(uri); - } - async del(uri: URI): TPromise { - deletedResources.add(uri); - } - }; - + const fileService = new TestFileService(); const textFileService = new class extends mock() { isDirty() { return false; } + create(uri: URI, contents?: string, options?: any) { + createdResources.add(uri); + return TPromise.as(void 0); + } + delete(resource: URI) { + deletedResources.add(resource); + return TPromise.as(void 0); + } + move(source: URI, target: URI) { + movedResources.set(source, target); + return TPromise.as(void 0); + } models = { onModelSaved: Event.None, onModelReverted: Event.None, @@ -72,8 +74,17 @@ suite('MainThreadEditors', () => { }; const workbenchEditorService = new TestEditorService(); const editorGroupService = new TestEditorGroupsService(); + const textModelService = new class extends mock() { + createModelReference(resource: URI): TPromise> { + const textEditorModel: ITextEditorModel = new class extends mock() { + textEditorModel = modelService.getModel(resource); + }; + textEditorModel.isReadonly = () => false; + return TPromise.as(new ImmortalReference(textEditorModel)); + } + }; - const bulkEditService = new BulkEditService(modelService, new TestEditorService(), null, fileService); + const bulkEditService = new BulkEditService(new NullLogService(), modelService, new TestEditorService(), textModelService, new TestFileService(), textFileService, new LabelService(TestEnvironmentService, new TestContextService())); const rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(ExtHostContext.ExtHostDocuments, new class extends mock() { @@ -130,12 +141,44 @@ suite('MainThreadEditors', () => { }); }); + test(`issue #54773: applyWorkspaceEdit checks model version in race situation`, () => { + + let model = modelService.createModel('something', null, resource); + + let workspaceResourceEdit1: ResourceTextEdit = { + resource: resource, + modelVersionId: model.getVersionId(), + edits: [{ + text: 'asdfg', + range: new Range(1, 1, 1, 1) + }] + }; + let workspaceResourceEdit2: ResourceTextEdit = { + resource: resource, + modelVersionId: model.getVersionId(), + edits: [{ + text: 'asdfg', + range: new Range(1, 1, 1, 1) + }] + }; + + let p1 = editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { + // first edit request succeeds + assert.equal(result, true); + }); + let p2 = editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { + // second edit request fails + assert.equal(result, false); + }); + return TPromise.join([p1, p2]); + }); + test(`applyWorkspaceEdit with only resource edit`, () => { return editors.$tryApplyWorkspaceEdit({ edits: [ - { oldUri: resource, newUri: resource }, - { oldUri: undefined, newUri: resource }, - { oldUri: resource, newUri: undefined } + { oldUri: resource, newUri: resource, options: undefined }, + { oldUri: undefined, newUri: resource, options: undefined }, + { oldUri: resource, newUri: undefined, options: undefined } ] }).then((result) => { assert.equal(result, true); @@ -145,15 +188,3 @@ suite('MainThreadEditors', () => { }); }); }); - - -function createMockFileStat(target: URI): IFileStat { - return { - etag: '', - isDirectory: false, - name: target.path, - mtime: 0, - resource: target - }; -} - diff --git a/src/vs/workbench/test/electron-browser/api/testRPCProtocol.ts b/src/vs/workbench/test/electron-browser/api/testRPCProtocol.ts index f7aa186cddd..af6d6512941 100644 --- a/src/vs/workbench/test/electron-browser/api/testRPCProtocol.ts +++ b/src/vs/workbench/test/electron-browser/api/testRPCProtocol.ts @@ -6,10 +6,12 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; +import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { CharCode } from 'vs/base/common/charCode'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { isThenable } from 'vs/base/common/async'; -export function SingleProxyRPCProtocol(thing: any): IRPCProtocol { +export function SingleProxyRPCProtocol(thing: any): IExtHostContext { return { getProxy(): T { return thing; @@ -23,7 +25,7 @@ export function SingleProxyRPCProtocol(thing: any): IRPCProtocol { declare var Proxy: any; // TODO@TypeScript -export class TestRPCProtocol implements IRPCProtocol { +export class TestRPCProtocol implements IExtHostContext { private _callCountValue: number = 0; private _idle: Promise; @@ -68,10 +70,10 @@ export class TestRPCProtocol implements IRPCProtocol { } public getProxy(identifier: ProxyIdentifier): T { - if (!this._proxies[identifier.id]) { - this._proxies[identifier.id] = this._createProxy(identifier.id); + if (!this._proxies[identifier.sid]) { + this._proxies[identifier.sid] = this._createProxy(identifier.sid); } - return this._proxies[identifier.id]; + return this._proxies[identifier.sid]; } private _createProxy(proxyId: string): T { @@ -89,7 +91,7 @@ export class TestRPCProtocol implements IRPCProtocol { } public set(identifier: ProxyIdentifier, value: R): R { - this._locals[identifier.id] = value; + this._locals[identifier.sid] = value; return value; } @@ -105,7 +107,7 @@ export class TestRPCProtocol implements IRPCProtocol { let p: Thenable; try { let result = (instance[path]).apply(instance, wireArgs); - p = TPromise.is(result) ? result : TPromise.as(result); + p = isThenable(result) ? result : TPromise.as(result); } catch (err) { p = TPromise.wrapError(err); } diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index fe9c4ac96b0..2a61d2a9ed0 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -12,7 +12,6 @@ import { createSyncDescriptor } from 'vs/platform/instantiation/common/descripto import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { ISearchService } from 'vs/platform/search/common/search'; import { ITelemetryService, ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { IExperimentService, IExperiments } from 'vs/platform/telemetry/common/experiments'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import * as minimist from 'minimist'; @@ -24,13 +23,14 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { TestEnvironmentService, TestContextService, TestEditorService, TestEditorGroupsService } from 'vs/workbench/test/workbenchTestServices'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { CancellationToken } from 'vs/base/common/cancellation'; namespace Timer { export interface ITimerEvent { @@ -68,11 +68,9 @@ suite.skip('QuickOpen performance (integration)', () => { const testWorkspacePath = testWorkspaceArg ? path.resolve(testWorkspaceArg) : __dirname; const telemetryService = new TestTelemetryService(); - const experimentService = new TestExperimentService(); const configurationService = new TestConfigurationService(); const instantiationService = new InstantiationService(new ServiceCollection( [ITelemetryService, telemetryService], - [IExperimentService, experimentService], [IConfigurationService, configurationService], [IModelService, new ModelServiceImpl(null, configurationService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], @@ -90,7 +88,7 @@ suite.skip('QuickOpen performance (integration)', () => { function measure() { const handler = descriptor.instantiate(instantiationService); handler.onOpen(); - return handler.getResults('a').then(result => { + return handler.getResults('a', CancellationToken.None).then(result => { const uncachedEvent = popEvent(); assert.strictEqual(uncachedEvent.data.symbols.fromCache, false, 'symbols.fromCache'); assert.strictEqual(uncachedEvent.data.files.fromCache, true, 'files.fromCache'); @@ -99,7 +97,7 @@ suite.skip('QuickOpen performance (integration)', () => { } return uncachedEvent; }).then(uncachedEvent => { - return handler.getResults('ab').then(result => { + return handler.getResults('ab', CancellationToken.None).then(result => { const cachedEvent = popEvent(); assert.strictEqual(uncachedEvent.data.symbols.fromCache, false, 'symbols.fromCache'); assert.ok(cachedEvent.data.files.fromCache, 'filesFromCache'); @@ -179,12 +177,3 @@ class TestTelemetryService implements ITelemetryService { }); } } - -class TestExperimentService implements IExperimentService { - - _serviceBrand: any; - - getExperiments(): IExperiments { - return {}; - } -} diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index e94ff2d8c1f..5ff0e54e5ce 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -22,7 +22,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { TestEnvironmentService, TestContextService, TestEditorService, TestEditorGroupsService } from 'vs/workbench/test/workbenchTestServices'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index ce5053d10fe..0a98d5655b7 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -7,10 +7,11 @@ import 'vs/workbench/parts/files/electron-browser/files.contribution'; // load our contribution into the test import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; -import { Promise, TPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import * as paths from 'vs/base/common/paths'; -import URI from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService'; @@ -26,7 +27,7 @@ import { TextModelResolverService } from 'vs/workbench/services/textmodelResolve import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, Workspace } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; @@ -41,19 +42,18 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, MenuBarVisibility } from 'vs/platform/windows/common/windows'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { isLinux } from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; -import { ICommandAction, IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; @@ -64,15 +64,19 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IExtensionService, ProfileSession, IExtensionsStatus, ExtensionPointContribution, IExtensionDescription } from '../services/extensions/common/extensions'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IKeybindingService } from '../../platform/keybinding/common/keybinding'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout } from 'vs/workbench/services/group/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; +import { Dimension } from 'vs/base/browser/dom'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { ILabelService, LabelService } from 'vs/platform/label/common/label'; +import { timeout } from 'vs/base/common/async'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -83,7 +87,7 @@ export const TestEnvironmentService = new EnvironmentService(parseArgs(process.a export class TestContextService implements IWorkspaceContextService { public _serviceBrand: any; - private workspace: IWorkbenchWorkspace; + private workspace: Workspace; private options: any; private readonly _onDidChangeWorkspaceName: Emitter; @@ -130,7 +134,7 @@ export class TestContextService implements IWorkspaceContextService { } public getWorkspaceFolder(resource: URI): IWorkspaceFolder { - return this.isInsideWorkspace(resource) ? this.workspace.folders[0] : null; + return this.workspace.getFolder(resource); } public setWorkspace(workspace: any): void { @@ -147,7 +151,7 @@ export class TestContextService implements IWorkspaceContextService { public isInsideWorkspace(resource: URI): boolean { if (resource && this.workspace) { - return paths.isEqualOrParent(resource.fsPath, this.workspace.folders[0].uri.fsPath, !isLinux /* ignorecase */); + return resources.isEqualOrParent(resource, this.workspace.folders[0].uri); } return false; @@ -158,16 +162,7 @@ export class TestContextService implements IWorkspaceContextService { } public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { - return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier); - } - - private pathEquals(path1: string, path2: string): boolean { - if (!isLinux) { - path1 = path1.toLowerCase(); - path2 = path2.toLowerCase(); - } - - return path1 === path2; + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && resources.isEqual(this.workspace.folders[0].uri, workspaceIdentifier); } } @@ -248,7 +243,8 @@ export class TestTextFileService extends TextFileService { export function workbenchInstantiationService(): IInstantiationService { let instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); instantiationService.stub(IContextKeyService, instantiationService.createInstance(MockContextKeyService)); - instantiationService.stub(IWorkspaceContextService, new TestContextService(TestWorkspace)); + const workspaceContextService = new TestContextService(TestWorkspace); + instantiationService.stub(IWorkspaceContextService, workspaceContextService); const configService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, configService); instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); @@ -274,7 +270,9 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IEnvironmentService, TestEnvironmentService); instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IHashService, new TestHashService()); + instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService([new TestEditorGroup(0)])); + instantiationService.stub(ILabelService, new LabelService(TestEnvironmentService, workspaceContextService)); const editorService = new TestEditorService(); instantiationService.stub(IEditorService, editorService); instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); @@ -282,10 +280,23 @@ export function workbenchInstantiationService(): IInstantiationService { return instantiationService; } +export class TestLogService implements ILogService { + _serviceBrand: any; onDidChangeLogLevel: Event; + getLevel(): LogLevel { return LogLevel.Info; } + setLevel(level: LogLevel): void { } + trace(message: string, ...args: any[]): void { } + debug(message: string, ...args: any[]): void { } + info(message: string, ...args: any[]): void { } + warn(message: string, ...args: any[]): void { } + error(message: string | Error, ...args: any[]): void { } + critical(message: string | Error, ...args: any[]): void { } + dispose(): void { } +} + export class TestDecorationsService implements IDecorationsService { _serviceBrand: any; onDidChangeDecorations: Event = Event.None; - registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { return toDisposable(); } + registerDecorationsProvider(provider: IDecorationsProvider): IDisposable { return Disposable.None; } getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration { return void 0; } } @@ -293,13 +304,13 @@ export class TestExtensionService implements IExtensionService { _serviceBrand: any; onDidRegisterExtensions: Event = Event.None; onDidChangeExtensionsStatus: Event = Event.None; - activateByEvent(activationEvent: string): Promise { return TPromise.as(void 0); } - whenInstalledExtensionsRegistered(): Promise { return TPromise.as(true); } - getExtensions(): Promise { return TPromise.as([]); } - readExtensionPointContributions(extPoint: IExtensionPoint): Promise[]> { return TPromise.as(Object.create(null)); } + activateByEvent(activationEvent: string): TPromise { return TPromise.as(void 0); } + whenInstalledExtensionsRegistered(): TPromise { return TPromise.as(true); } + getExtensions(): TPromise { return TPromise.as([]); } + readExtensionPointContributions(extPoint: IExtensionPoint): TPromise[]> { return TPromise.as(Object.create(null)); } getExtensionsStatus(): { [id: string]: IExtensionsStatus; } { return Object.create(null); } canProfileExtensionHost(): boolean { return false; } - startExtensionHostProfile(): Promise { return TPromise.as(Object.create(null)); } + startExtensionHostProfile(): TPromise { return TPromise.as(Object.create(null)); } restartExtensionHost(): void { } startExtensionHost(): void { } stopExtensionHost(): void { } @@ -347,11 +358,11 @@ export class TestHistoryService implements IHistoryService { return []; } - public getLastActiveWorkspaceRoot(schemeFilter?: string): URI { + public getLastActiveWorkspaceRoot(schemeFilter: string): URI { return this.root; } - public getLastActiveFile(): URI { + public getLastActiveFile(schemeFilter: string): URI { return void 0; } } @@ -360,11 +371,11 @@ export class TestDialogService implements IDialogService { public _serviceBrand: any; - public confirm(confirmation: IConfirmation): Promise { + public confirm(confirmation: IConfirmation): TPromise { return TPromise.as({ confirmed: false }); } - public show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { + public show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): TPromise { return TPromise.as(0); } } @@ -374,18 +385,21 @@ export class TestPartService implements IPartService { public _serviceBrand: any; private _onTitleBarVisibilityChange = new Emitter(); + private _onMenubarVisibilityChange = new Emitter(); private _onEditorLayout = new Emitter(); public get onTitleBarVisibilityChange(): Event { return this._onTitleBarVisibilityChange.event; } + public get onMenubarVisibilityChange(): Event { + return this._onMenubarVisibilityChange.event; + } + public get onEditorLayout(): Event { return this._onEditorLayout.event; } - public layout(): void { } - public isCreated(): boolean { return true; } @@ -438,6 +452,10 @@ export class TestPartService implements IPartService { return false; } + public getMenubarVisibility(): MenuBarVisibility { + return null; + } + public getSideBarPosition() { return 0; } @@ -452,7 +470,7 @@ export class TestPartService implements IPartService { public addClass(clazz: string): void { } public removeClass(clazz: string): void { } - public getWorkbenchElementId(): string { return ''; } + public getWorkbenchElement(): HTMLElement { return void 0; } public toggleZenMode(): void { } @@ -584,7 +602,7 @@ export class TestEditorGroup implements IEditorGroupView { disposed: boolean; editors: ReadonlyArray = []; label: string; - whenRestored: Promise = TPromise.as(void 0); + whenRestored: TPromise = TPromise.as(void 0); element: HTMLElement; minimumWidth: number; maximumWidth: number; @@ -667,6 +685,7 @@ export class TestEditorGroup implements IEditorGroupView { dispose(): void { } toJSON(): object { return Object.create(null); } layout(width: number, height: number): void { } + relayout() { } } export class TestEditorService implements EditorServiceImpl { @@ -806,16 +825,14 @@ export class TestFileService implements IFileService { } updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { - return TPromise.timeout(1).then(() => { - return { - resource, - etag: 'index.txt', - encoding: 'utf8', - mtime: Date.now(), - isDirectory: false, - name: paths.basename(resource.fsPath) - }; - }); + return TPromise.wrap(timeout(0).then(() => ({ + resource, + etag: 'index.txt', + encoding: 'utf8', + mtime: Date.now(), + isDirectory: false, + name: paths.basename(resource.fsPath) + }))); } moveFile(source: URI, target: URI, overwrite?: boolean): TPromise { @@ -834,10 +851,6 @@ export class TestFileService implements IFileService { return TPromise.as(null); } - rename(resource: URI, newName: string): TPromise { - return TPromise.as(null); - } - onDidChangeFileSystemProviderRegistrations = Event.None; registerProvider(scheme: string, provider) { @@ -848,7 +861,7 @@ export class TestFileService implements IFileService { return resource.scheme === 'file'; } - del(resource: URI, useTrash?: boolean): TPromise { + del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise { return TPromise.as(null); } @@ -869,8 +882,6 @@ export class TestFileService implements IFileService { export class TestBackupFileService implements IBackupFileService { public _serviceBrand: any; - public backupEnabled: boolean; - public hasBackups(): TPromise { return TPromise.as(false); } @@ -958,11 +969,16 @@ export class TestWindowService implements IWindowService { public _serviceBrand: any; onDidChangeFocus: Event = new Emitter().event; + onDidChangeMaximize: Event; isFocused(): TPromise { return TPromise.as(false); } + isMaximized(): TPromise { + return TPromise.as(false); + } + getConfiguration(): IWindowConfiguration { return Object.create(null); } @@ -1003,6 +1019,10 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } + enterWorkspace(path: string): TPromise { + return TPromise.as(void 0); + } + createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return TPromise.as(void 0); } @@ -1027,7 +1047,19 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + maximizeWindow(): TPromise { + return TPromise.as(void 0); + } + + unmaximizeWindow(): TPromise { + return TPromise.as(void 0); + } + + minimizeWindow(): TPromise { + return TPromise.as(void 0); + } + + openWindow(paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { return TPromise.as(void 0); } @@ -1059,7 +1091,7 @@ export class TestWindowService implements IWindowService { return TPromise.wrap(void 0); } - updateTouchBar(items: ICommandAction[][]): Promise { + updateTouchBar(items: ISerializableCommandAction[][]): TPromise { return TPromise.as(void 0); } } @@ -1104,6 +1136,9 @@ export class TestWindowsService implements IWindowsService { onWindowOpen: Event; onWindowFocus: Event; onWindowBlur: Event; + onWindowMaximize: Event; + onWindowUnmaximize: Event; + onRecentlyOpenedChange: Event; isFocused(windowId: number): TPromise { return TPromise.as(false); @@ -1141,6 +1176,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + enterWorkspace(windowId: number, path: string): TPromise { + return TPromise.as(void 0); + } + createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return TPromise.as(void 0); } @@ -1157,11 +1196,11 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - addRecentlyOpened(files: string[]): TPromise { + addRecentlyOpened(files: URI[]): TPromise { return TPromise.as(void 0); } - removeFromRecentlyOpened(paths: string[]): TPromise { + removeFromRecentlyOpened(paths: URI[]): TPromise { return TPromise.as(void 0); } @@ -1189,6 +1228,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + minimizeWindow(windowId: number): TPromise { + return TPromise.as(void 0); + } + unmaximizeWindow(windowId: number): TPromise { return TPromise.as(void 0); } @@ -1218,7 +1261,7 @@ export class TestWindowsService implements IWindowsService { } // Global methods - openWindow(windowId: number, paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { + openWindow(windowId: number, paths: URI[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { return TPromise.as(void 0); } @@ -1230,7 +1273,7 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderPath?: string; title: string; filename?: string; }[]> { + getWindows(): TPromise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { return TPromise.as(void 0); } @@ -1246,30 +1289,38 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } - showPreviousWindowTab(): Promise { + newWindowTab(): TPromise { return TPromise.as(void 0); } - showNextWindowTab(): Promise { + showPreviousWindowTab(): TPromise { return TPromise.as(void 0); } - moveWindowTabToNewWindow(): Promise { + showNextWindowTab(): TPromise { return TPromise.as(void 0); } - mergeAllWindowTabs(): Promise { + moveWindowTabToNewWindow(): TPromise { return TPromise.as(void 0); } - toggleWindowTabsBar(): Promise { + mergeAllWindowTabs(): TPromise { return TPromise.as(void 0); } - updateTouchBar(windowId: number, items: ICommandAction[][]): Promise { + toggleWindowTabsBar(): TPromise { return TPromise.as(void 0); } + updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): TPromise { + return TPromise.as(void 0); + } + + getActiveWindowId(): TPromise { + return TPromise.as(undefined); + } + // This needs to be handled from browser process to prevent // foreground ordering issues on Windows openExternal(url: string): TPromise { diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 5feaad584c7..ce9193eb9cf 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -16,7 +16,8 @@ import 'vs/workbench/services/configuration/common/configurationExtensionPoint'; import 'vs/editor/editor.all'; // Platform -import 'vs/platform/widget/browser/widget.contribution'; +import 'vs/platform/widget/browser/contextScopedHistoryWidget'; +import 'vs/platform/label/electron-browser/label.contribution'; // Menus/Actions import 'vs/workbench/services/actions/electron-browser/menusExtensionPoint'; @@ -54,6 +55,8 @@ import 'vs/workbench/parts/backup/common/backup.contribution'; import 'vs/workbench/parts/stats/node/stats.contribution'; +import 'vs/workbench/parts/splash/electron-browser/partsSplash.contribution'; + import 'vs/workbench/parts/search/electron-browser/search.contribution'; import 'vs/workbench/parts/search/browser/searchView'; // can be packaged separately import 'vs/workbench/parts/search/browser/openAnythingHandler'; // can be packaged separately @@ -64,7 +67,6 @@ import 'vs/workbench/parts/scm/electron-browser/scmViewlet'; // can be packaged 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/debugEditorActions'; import 'vs/workbench/parts/debug/browser/debugViewlet'; // can be packaged separately import 'vs/workbench/parts/markers/electron-browser/markers.contribution'; @@ -138,5 +140,8 @@ import 'vs/workbench/parts/welcome/overlay/browser/welcomeOverlay'; import 'vs/workbench/parts/outline/electron-browser/outline.contribution'; +import 'vs/workbench/parts/navigation/common/navigation.contribution'; + // services import 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService'; +import 'vs/workbench/parts/experiments/electron-browser/experiments.contribution'; diff --git a/test/all.js b/test/all.js index 14c6897b197..915d68a6309 100644 --- a/test/all.js +++ b/test/all.js @@ -49,9 +49,10 @@ function main() { nodeMain: __filename, baseUrl: path.join(path.dirname(__dirname), 'src'), paths: { + 'vs/css': '../test/css.mock', 'vs': `../${ out }/vs`, 'lib': `../${ out }/lib`, - 'bootstrap': `../${ out }/bootstrap` + 'bootstrap-fork': `../${ out }/bootstrap-fork` }, catchError: true }; diff --git a/test/css.mock.js b/test/css.mock.js new file mode 100644 index 00000000000..1829c6ae48e --- /dev/null +++ b/test/css.mock.js @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +define([], function() { + return { + load: function(name, req, load) { + load({}); + } + }; +}); diff --git a/test/electron/index.js b/test/electron/index.js index ee98b8f8ea3..79c7136ca4c 100644 --- a/test/electron/index.js +++ b/test/electron/index.js @@ -9,6 +9,7 @@ const { join } = require('path'); const path = require('path'); const mocha = require('mocha'); const events = require('events'); +const MochaJUnitReporter = require('mocha-junit-reporter'); const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec'; @@ -21,7 +22,7 @@ const optimist = require('optimist') .describe('debug', 'open dev tools, keep window open, reuse app data').string('debug') .describe('reporter', 'the mocha reporter').string('reporter').default('reporter', defaultReporterName) .describe('reporter-options', 'the mocha reporter options').string('reporter-options').default('reporter-options', '') - .describe('tfs').boolean('tfs') + .describe('tfs').string('tfs') .describe('help', 'show the help').alias('help', 'h'); const argv = optimist.argv; @@ -96,26 +97,6 @@ function parseReporterOption(value) { return r ? { [r[1]]: r[2] } : {}; } -class TFSReporter extends mocha.reporters.Base { - - constructor(runner) { - super(runner); - - runner.on('pending', test => { - console.log('PEND', test.fullTitle()); - }); - runner.on('pass', test => { - console.log('OK ', test.fullTitle(), `(${test.duration}ms)`); - }); - runner.on('fail', test => { - console.log('FAIL', test.fullTitle(), `(${test.duration}ms)`); - }); - runner.once('end', () => { - this.epilogue(); - }); - } -} - app.on('ready', () => { const win = new BrowserWindow({ @@ -141,7 +122,13 @@ app.on('ready', () => { const runner = new IPCRunner(); if (argv.tfs) { - new TFSReporter(runner); + new mocha.reporters.Spec(runner); + new MochaJUnitReporter(runner, { + reporterOptions: { + testsuitesTitle: `${argv.tfs} ${process.platform}`, + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + } + }); } else { const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter); let Reporter; diff --git a/test/electron/renderer.js b/test/electron/renderer.js index 63729010808..40aa7ed434a 100644 --- a/test/electron/renderer.js +++ b/test/electron/renderer.js @@ -13,6 +13,7 @@ const minimatch = require('minimatch'); const istanbul = require('istanbul'); const i_remap = require('remap-istanbul/lib/remap'); const util = require('util'); +const bootstrap = require('../../src/bootstrap'); // Disabled custom inspect. See #38847 if (util.inspect && util.inspect['defaultOptions']) { @@ -33,11 +34,11 @@ function initLoader(opts) { nodeRequire: require, nodeMain: __filename, catchError: true, - baseUrl: path.join(__dirname, '../../src'), + baseUrl: bootstrap.uriFromPath(path.join(__dirname, '../../src')), paths: { 'vs': `../${outdir}/vs`, 'lib': `../${outdir}/lib`, - 'bootstrap': `../${outdir}/bootstrap` + 'bootstrap-fork': `../${outdir}/bootstrap-fork` } }; diff --git a/test/smoke/package.json b/test/smoke/package.json index e033db34da0..857e7a49440 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -9,7 +9,7 @@ "copy-driver": "cpx src/vscode/driver.js out/vscode", "watch-driver": "cpx src/vscode/driver.js out/vscode -w", "copy-driver-definition": "node tools/copy-driver-definition.js", - "watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/common", + "watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/node", "mocha": "mocha" }, "devDependencies": { @@ -22,16 +22,18 @@ "@types/webdriverio": "4.6.1", "concurrently": "^3.5.1", "cpx": "^1.5.0", - "electron": "1.7.7", + "electron": "^2.0.6", "htmlparser2": "^3.9.2", "mkdirp": "^0.5.1", - "mocha": "^3.2.0", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", "ncp": "^2.0.0", "portastic": "^1.0.1", "rimraf": "^2.6.1", "strip-json-comments": "^2.0.1", "tmp": "0.0.33", - "typescript": "2.5.2", + "typescript": "2.9.2", "watch": "^1.0.2" } -} +} \ No newline at end of file diff --git a/test/smoke/src/application.ts b/test/smoke/src/application.ts index 7e08c89d140..d7505a0816e 100644 --- a/test/smoke/src/application.ts +++ b/test/smoke/src/application.ts @@ -8,7 +8,7 @@ import * as cp from 'child_process'; import { Code, spawn, SpawnOptions } from './vscode/code'; import { Logger } from './logger'; -export enum Quality { +export const enum Quality { Dev, Insiders, Stable diff --git a/test/smoke/src/areas/activitybar/activityBar.ts b/test/smoke/src/areas/activitybar/activityBar.ts index a2acb1ac44d..f055ad69621 100644 --- a/test/smoke/src/areas/activitybar/activityBar.ts +++ b/test/smoke/src/areas/activitybar/activityBar.ts @@ -5,7 +5,7 @@ import { Code } from '../../vscode/code'; -export enum ActivityBarPosition { +export const enum ActivityBarPosition { LEFT = 0, RIGHT = 1 } diff --git a/test/smoke/src/areas/debug/debug.ts b/test/smoke/src/areas/debug/debug.ts index 312d99ce0db..4dc52619757 100644 --- a/test/smoke/src/areas/debug/debug.ts +++ b/test/smoke/src/areas/debug/debug.ts @@ -23,9 +23,9 @@ const BREAKPOINT_GLYPH = '.debug-breakpoint'; const PAUSE = `.debug-actions-widget .debug-action.pause`; const DEBUG_STATUS_BAR = `.statusbar.debugging`; const NOT_DEBUG_STATUS_BAR = `.statusbar:not(debugging)`; -const TOOLBAR_HIDDEN = `.debug-actions-widget.monaco-builder-hidden`; +const TOOLBAR_HIDDEN = `.debug-actions-widget[aria-hidden="true"]`; const STACK_FRAME = `${VIEWLET} .monaco-tree-row .stack-frame`; -const SPECIFIC_STACK_FRAME = filename => `${STACK_FRAME} .file[title$="${filename}"]`; +const SPECIFIC_STACK_FRAME = filename => `${STACK_FRAME} .file[title*="${filename}"]`; const VARIABLE = `${VIEWLET} .debug-variables .monaco-tree-row .expression`; const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_INPUT_OUTPUT = `.repl .input-output-pair .output.expression .value`; @@ -130,7 +130,7 @@ export class Debug extends Viewlet { await this.code.waitForSetValue(REPL_FOCUSED, text); // Wait for the keys to be picked up by the editor model such that repl evalutes what just got typed - await this.editor.waitForEditorContents('debug:input', s => s.indexOf(text) >= 0); + await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); await this.code.dispatchKeybinding('enter'); await this.code.waitForElement(CONSOLE_INPUT_OUTPUT); await this.waitForOutput(output => accept(output[output.length - 1] || '')); diff --git a/test/smoke/src/areas/extensions/extensions.ts b/test/smoke/src/areas/extensions/extensions.ts index 199242f8f50..b3ad16827c8 100644 --- a/test/smoke/src/areas/extensions/extensions.ts +++ b/test/smoke/src/areas/extensions/extensions.ts @@ -6,7 +6,7 @@ import { Viewlet } from '../workbench/viewlet'; import { Code } from '../../vscode/code'; -const SEARCH_BOX = 'div.extensions-viewlet[id="workbench.view.extensions"] input.search-box'; +const SEARCH_BOX = 'div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea'; export class Extensions extends Viewlet { @@ -27,12 +27,13 @@ export class Extensions extends Viewlet { async searchForExtension(name: string): Promise { await this.code.waitAndClick(SEARCH_BOX); await this.code.waitForActiveElement(SEARCH_BOX); - await this.code.waitForSetValue(SEARCH_BOX, `name:"${name}"`); + await this.code.waitForTypeInEditor(SEARCH_BOX, `name:"${name}"`); } async installExtension(name: string): Promise { await this.searchForExtension(name); - await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.install`); - await this.code.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${name}"] .extension li[class='action-item'] .extension-action.reload`); + const ariaLabel = `${name}. Press enter for extension details.`; + await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${ariaLabel}"] .extension li[class='action-item'] .extension-action.install`); + await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.reload`); } } \ No newline at end of file diff --git a/test/smoke/src/areas/git/git.test.ts b/test/smoke/src/areas/git/git.test.ts index 8846db475a2..208301c012e 100644 --- a/test/smoke/src/areas/git/git.test.ts +++ b/test/smoke/src/areas/git/git.test.ts @@ -48,24 +48,19 @@ export function setup() { const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); - await app.workbench.scm.waitForChange('app.js', 'Modified'); + await app.workbench.scm.stage('app.js'); - - await app.workbench.scm.waitForChange('app.js', 'Index Modified'); await app.workbench.scm.unstage('app.js'); - - await app.workbench.scm.waitForChange('app.js', 'Modified'); }); it(`stages, commits changes and verifies outgoing change`, async function () { const app = this.app as Application; await app.workbench.scm.openSCMViewlet(); - await app.workbench.scm.waitForChange('app.js', 'Modified'); + await app.workbench.scm.stage('app.js'); - await app.workbench.scm.waitForChange('app.js', 'Index Modified'); await app.workbench.scm.commit('first commit'); await app.code.waitForTextContent(SYNC_STATUSBAR, ' 0↓ 1↑'); diff --git a/test/smoke/src/areas/git/scm.ts b/test/smoke/src/areas/git/scm.ts index 0107b900ebd..ee6b16c73c8 100644 --- a/test/smoke/src/areas/git/scm.ts +++ b/test/smoke/src/areas/git/scm.ts @@ -10,12 +10,10 @@ import { findElement, findElements, Code } from '../../vscode/code'; const VIEWLET = 'div[id="workbench.view.scm"]'; const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row > .resource`; -const SCM_RESOURCE_GROUP = `${VIEWLET} .monaco-list-row > .resource-group`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[title="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[title="Commit"]`; const SCM_RESOURCE_CLICK = (name: string) => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"] .label-name`; const SCM_RESOURCE_ACTION_CLICK = (name: string, actionName: string) => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"] .actions .action-label[title="${actionName}"]`; -const SCM_RESOURCE_GROUP_COMMAND_CLICK = (name: string) => `${SCM_RESOURCE_GROUP} .actions .action-label[title="${name}"]`; interface Change { name: string; @@ -64,14 +62,12 @@ export class SCM extends Viewlet { async stage(name: string): Promise { await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Stage Changes')); - } - - async stageAll(): Promise { - await this.code.waitAndClick(SCM_RESOURCE_GROUP_COMMAND_CLICK('Stage All Changes')); + await this.waitForChange(name, 'Index Modified'); } async unstage(name: string): Promise { await this.code.waitAndClick(SCM_RESOURCE_ACTION_CLICK(name, 'Unstage Changes')); + await this.waitForChange('app.js', 'Modified'); } async commit(message: string): Promise { diff --git a/test/smoke/src/areas/preferences/keybindings.ts b/test/smoke/src/areas/preferences/keybindings.ts index 235621574f0..c9f1273d0c1 100644 --- a/test/smoke/src/areas/preferences/keybindings.ts +++ b/test/smoke/src/areas/preferences/keybindings.ts @@ -21,14 +21,14 @@ export class KeybindingsEditor { await this.code.waitForActiveElement(SEARCH_INPUT); await this.code.waitForSetValue(SEARCH_INPUT, command); - await this.code.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item'); - await this.code.waitForElement('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item.focused.selected'); + await this.code.waitAndClick('.keybindings-list-container .monaco-list-row.keybinding-item'); + await this.code.waitForElement('.keybindings-list-container .monaco-list-row.keybinding-item.focused.selected'); - await this.code.waitAndClick('div[aria-label="Keybindings"] .monaco-list-row.keybinding-item .action-item .icon.add'); + await this.code.waitAndClick('.keybindings-list-container .monaco-list-row.keybinding-item .action-item .icon.add'); await this.code.waitForActiveElement('.defineKeybindingWidget .monaco-inputbox input'); await this.code.dispatchKeybinding(keybinding); await this.code.dispatchKeybinding('enter'); - await this.code.waitForElement(`div[aria-label="Keybindings"] div[aria-label="Keybinding is ${ariaLabel}."]`); + await this.code.waitForElement(`.keybindings-list-container div[aria-label="Keybinding is ${ariaLabel}."]`); } } \ No newline at end of file diff --git a/test/smoke/src/areas/preferences/settings.ts b/test/smoke/src/areas/preferences/settings.ts index ecdef71c033..525d3774bcb 100644 --- a/test/smoke/src/areas/preferences/settings.ts +++ b/test/smoke/src/areas/preferences/settings.ts @@ -10,7 +10,7 @@ import { Editors } from '../editor/editors'; import { Code } from '../../vscode/code'; import { QuickOpen } from '../quickopen/quickopen'; -export enum ActivityBarPosition { +export const enum ActivityBarPosition { LEFT = 0, RIGHT = 1 } @@ -42,6 +42,6 @@ export class SettingsEditor { } private async openSettings(): Promise { - await this.quickopen.runCommand('Preferences: Open User Settings'); + await this.quickopen.runCommand('Preferences: Open Settings (JSON)'); } } \ No newline at end of file diff --git a/test/smoke/src/areas/problems/problems.ts b/test/smoke/src/areas/problems/problems.ts index b31ac96c572..bffd6a95fca 100644 --- a/test/smoke/src/areas/problems/problems.ts +++ b/test/smoke/src/areas/problems/problems.ts @@ -5,7 +5,7 @@ import { Code } from '../../vscode/code'; -export enum ProblemSeverity { +export const enum ProblemSeverity { WARNING = 0, ERROR = 1 } diff --git a/test/smoke/src/areas/quickinput/quickinput.ts b/test/smoke/src/areas/quickinput/quickinput.ts index c13b308c0d9..aab4253c970 100644 --- a/test/smoke/src/areas/quickinput/quickinput.ts +++ b/test/smoke/src/areas/quickinput/quickinput.ts @@ -25,4 +25,13 @@ export class QuickInput { private async waitForQuickInputClosed(): Promise { await this.code.waitForElement(QuickInput.QUICK_INPUT, r => !!r && r.attributes.style.indexOf('display: none;') !== -1); } + + async selectQuickInputElement(index: number): Promise { + await this.waitForQuickInputOpened(); + for (let from = 0; from < index; from++) { + await this.code.dispatchKeybinding('down'); + } + await this.code.dispatchKeybinding('enter'); + await this.waitForQuickInputClosed(); + } } diff --git a/test/smoke/src/areas/quickopen/quickopen.ts b/test/smoke/src/areas/quickopen/quickopen.ts index e46dbdfaeae..d9989302e4b 100644 --- a/test/smoke/src/areas/quickopen/quickopen.ts +++ b/test/smoke/src/areas/quickopen/quickopen.ts @@ -8,8 +8,8 @@ import { Code } from '../../vscode/code'; export class QuickOpen { + static QUICK_OPEN = 'div.monaco-quick-open-widget'; static QUICK_OPEN_HIDDEN = 'div.monaco-quick-open-widget[aria-hidden="true"]'; - static QUICK_OPEN = 'div.monaco-quick-open-widget[aria-hidden="false"]'; static QUICK_OPEN_INPUT = `${QuickOpen.QUICK_OPEN} .quick-open-input input`; static QUICK_OPEN_FOCUSED_ELEMENT = `${QuickOpen.QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label`; static QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry'; diff --git a/test/smoke/src/areas/search/search.ts b/test/smoke/src/areas/search/search.ts index 9238eeed6e6..e93328d086f 100644 --- a/test/smoke/src/areas/search/search.ts +++ b/test/smoke/src/areas/search/search.ts @@ -53,7 +53,7 @@ export class Search extends Viewlet { await this.waitForInputFocus(INPUT); await this.code.dispatchKeybinding('enter'); - await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="false"]`); + await this.code.waitForElement(`${VIEWLET} .messages`); } async setFilesToIncludeText(text: string): Promise { @@ -77,6 +77,8 @@ export class Search extends Viewlet { () => this.code.waitForElement(`${fileMatch} .action-label.icon.action-remove`, el => !!el && el.top > 0 && el.left > 0, 10) ); + // ¯\_(ツ)_/¯ + await new Promise(c => setTimeout(c, 500)); await this.code.waitAndClick(`${fileMatch} .action-label.icon.action-remove`); await this.code.waitForElement(fileMatch, el => !el); } @@ -101,15 +103,17 @@ export class Search extends Viewlet { () => this.code.waitForElement(`${fileMatch} .action-label.icon.action-replace-all`, el => !!el && el.top > 0 && el.left > 0, 10) ); + // ¯\_(ツ)_/¯ + await new Promise(c => setTimeout(c, 500)); await this.code.waitAndClick(`${fileMatch} .action-label.icon.action-replace-all`); } async waitForResultText(text: string): Promise { - await this.code.waitForTextContent(`${VIEWLET} .messages[aria-hidden="false"] .message>p`, text); + await this.code.waitForTextContent(`${VIEWLET} .messages .message>p`, text); } async waitForNoResultText(): Promise { - await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="false"] .message>p`, el => !el); + await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="true"] .message>p`); } private async waitForInputFocus(selector: string): Promise { diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index dfb1d7550d3..b2212a92797 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -35,17 +35,17 @@ export function setup() { await app.workbench.quickopen.openFile('app.js'); await app.workbench.statusbar.clickOn(StatusBarElement.INDENTATION_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); - await app.workbench.quickopen.closeQuickOpen(); + await app.workbench.quickinput.waitForQuickInputOpened(); + await app.workbench.quickinput.closeQuickInput(); await app.workbench.statusbar.clickOn(StatusBarElement.ENCODING_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); - await app.workbench.quickopen.closeQuickOpen(); + await app.workbench.quickinput.waitForQuickInputOpened(); + await app.workbench.quickinput.closeQuickInput(); await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); - await app.workbench.quickopen.closeQuickOpen(); + await app.workbench.quickinput.waitForQuickInputOpened(); + await app.workbench.quickinput.closeQuickInput(); await app.workbench.statusbar.clickOn(StatusBarElement.LANGUAGE_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); - await app.workbench.quickopen.closeQuickOpen(); + await app.workbench.quickinput.waitForQuickInputOpened(); + await app.workbench.quickinput.closeQuickInput(); }); it(`verifies that 'Problems View' appears when clicking on 'Problems' status element`, async function () { @@ -84,8 +84,8 @@ export function setup() { await app.workbench.quickopen.openFile('app.js'); await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS); - await app.workbench.quickopen.waitForQuickOpenOpened(); - await app.workbench.quickopen.selectQuickOpenElement(1); + await app.workbench.quickinput.waitForQuickInputOpened(); + await app.workbench.quickinput.selectQuickInputElement(1); await app.workbench.statusbar.waitForEOL('CRLF'); }); diff --git a/test/smoke/src/areas/statusbar/statusbar.ts b/test/smoke/src/areas/statusbar/statusbar.ts index dd3392a5a9c..b36678ea7d2 100644 --- a/test/smoke/src/areas/statusbar/statusbar.ts +++ b/test/smoke/src/areas/statusbar/statusbar.ts @@ -5,7 +5,7 @@ import { Code } from '../../vscode/code'; -export enum StatusBarElement { +export const enum StatusBarElement { BRANCH_STATUS = 0, SYNC_STATUS = 1, PROBLEMS_STATUS = 2, diff --git a/test/smoke/src/areas/terminal/terminal.ts b/test/smoke/src/areas/terminal/terminal.ts index baeae18556d..5c67cf5a9d6 100644 --- a/test/smoke/src/areas/terminal/terminal.ts +++ b/test/smoke/src/areas/terminal/terminal.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Code } from '../../vscode/code'; +import { QuickOpen } from '../quickopen/quickopen'; const PANEL_SELECTOR = 'div[id="workbench.panel.terminal"]'; const XTERM_SELECTOR = `${PANEL_SELECTOR} .terminal-wrapper`; @@ -11,10 +12,10 @@ const XTERM_TEXTAREA = `${XTERM_SELECTOR} textarea.xterm-helper-textarea`; export class Terminal { - constructor(private code: Code) { } + constructor(private code: Code, private quickopen: QuickOpen) { } async showTerminal(): Promise { - await this.code.dispatchKeybinding('ctrl+`'); + await this.quickopen.runCommand('View: Toggle Integrated Terminal'); await this.code.waitForActiveElement(XTERM_TEXTAREA); await this.code.waitForTerminalBuffer(XTERM_SELECTOR, lines => lines.some(line => line.length > 0)); } diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index ea7a25bf8cf..dace3cc683e 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -14,6 +14,10 @@ export function setup() { return; } + const extensionName = 'German Language Pack for Visual Studio Code'; + await app.workbench.extensions.openExtensionsViewlet(); + await app.workbench.extensions.installExtension(extensionName); + await app.restart({ extraArgs: ['--locale=DE'] }); }); diff --git a/test/smoke/src/areas/workbench/viewlet.ts b/test/smoke/src/areas/workbench/viewlet.ts index 2293752b705..84ccb601012 100644 --- a/test/smoke/src/areas/workbench/viewlet.ts +++ b/test/smoke/src/areas/workbench/viewlet.ts @@ -12,6 +12,6 @@ export abstract class Viewlet { constructor(protected code: Code) { } async waitForTitle(fn: (title: string) => boolean): Promise { - await this.code.waitForTextContent('.monaco-workbench-container .part.sidebar > .title > .title-label > span', undefined, fn); + await this.code.waitForTextContent('.monaco-workbench .part.sidebar > .title > .title-label > h2', undefined, fn); } } \ No newline at end of file diff --git a/test/smoke/src/areas/workbench/workbench.ts b/test/smoke/src/areas/workbench/workbench.ts index 005265f912c..1cb7a5023a0 100644 --- a/test/smoke/src/areas/workbench/workbench.ts +++ b/test/smoke/src/areas/workbench/workbench.ts @@ -57,7 +57,7 @@ export class Workbench { this.problems = new Problems(code); this.settingsEditor = new SettingsEditor(code, userDataPath, this.editors, this.editor, this.quickopen); this.keybindingsEditor = new KeybindingsEditor(code); - this.terminal = new Terminal(code); + this.terminal = new Terminal(code, this.quickopen); } } diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 6ca50fe319c..dd17406a724 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -273,7 +273,7 @@ describe('Test', () => { const app = this.app as Application; const raw = await app.capturePage(); - const buffer = new Buffer(raw, 'base64'); + const buffer = Buffer.from(raw, 'base64'); const name = this.currentTest.fullTitle().replace(/[^a-z0-9\-]/ig, '_'); const screenshotPath = path.join(screenshotsPath, `${name}.png`); diff --git a/test/smoke/src/vscode/code.ts b/test/smoke/src/vscode/code.ts index cfc4dd74c80..612405b948c 100644 --- a/test/smoke/src/vscode/code.ts +++ b/test/smoke/src/vscode/code.ts @@ -195,6 +195,10 @@ export class Code { ) { this.driver = new Proxy(driver, { get(target, prop, receiver) { + if (typeof prop === 'symbol') { + throw new Error('Invalid usage'); + } + if (typeof target[prop] !== 'function') { return target[prop]; } diff --git a/test/smoke/src/vscode/driver.js b/test/smoke/src/vscode/driver.js index 24af32d436f..1da40e42b06 100644 --- a/test/smoke/src/vscode/driver.js +++ b/test/smoke/src/vscode/driver.js @@ -7,6 +7,6 @@ const path = require('path'); exports.connect = function (outPath, handle) { const bootstrapPath = path.join(outPath, 'bootstrap-amd.js'); - const { bootstrap } = require(bootstrapPath); - return new Promise((c, e) => bootstrap('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e)); + const { load } = require(bootstrapPath); + return new Promise((c, e) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(c, e), e)); }; \ No newline at end of file diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js new file mode 100644 index 00000000000..71023e7e91f --- /dev/null +++ b/test/smoke/test/index.js @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const Mocha = require('mocha'); +const minimist = require('minimist'); + +const suite = 'Smoke Tests'; + +const [, , ...args] = process.argv; +const opts = minimist(args, { + string: [ + 'f' + ] +}); + +const options = { + useColors: true, + timeout: 60000, + slow: 30000, + grep: opts['f'] +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +const mocha = new Mocha(options); +mocha.addFile('out/main.js'); +mocha.run(failures => process.exit(failures ? -1 : 0)); diff --git a/test/smoke/test/mocha.opts b/test/smoke/test/mocha.opts deleted file mode 100644 index 7448824a9a6..00000000000 --- a/test/smoke/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---timeout 60000 ---slow 30000 -out/main.js \ No newline at end of file diff --git a/test/smoke/tools/copy-driver-definition.js b/test/smoke/tools/copy-driver-definition.js index fedf0c28432..643e60a67fe 100644 --- a/test/smoke/tools/copy-driver-definition.js +++ b/test/smoke/tools/copy-driver-definition.js @@ -7,7 +7,7 @@ const fs = require('fs'); const path = require('path'); const root = path.dirname(path.dirname(path.dirname(__dirname))); -const driverPath = path.join(root, 'src/vs/platform/driver/common/driver.ts'); +const driverPath = path.join(root, 'src/vs/platform/driver/node/driver.ts'); let contents = fs.readFileSync(driverPath, 'utf8'); contents = /\/\/\*START([\s\S]*)\/\/\*END/mi.exec(contents)[1].trim(); diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 2f15aec3278..57ac002dedb 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -41,9 +41,9 @@ version "8.0.33" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd" -"@types/node@^7.0.18": - version "7.0.46" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.46.tgz#c3dedd25558c676b3d6303e51799abb9c3f8f314" +"@types/node@^8.0.24": + version "8.10.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.23.tgz#e5ccfdafff42af5397c29669b6d7d65f7d629a00" "@types/rimraf@2.0.2": version "2.0.2" @@ -86,6 +86,10 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" @@ -238,9 +242,9 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" builtin-modules@^1.0.0: version "1.1.1" @@ -271,6 +275,10 @@ chalk@0.5.1: strip-ansi "^0.3.0" supports-color "^0.2.0" +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + chokidar@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -300,16 +308,14 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + commander@2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - dependencies: - graceful-readlink ">= 1.0.0" - commander@^2.8.1: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -367,6 +373,10 @@ cpx@^1.5.0: shell-quote "^1.6.1" subarg "^1.0.0" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -395,18 +405,18 @@ date-fns@^1.23.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - dependencies: - ms "2.0.0" - debug@2.6.9, debug@^2.1.3, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" +debug@3.1.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -427,9 +437,9 @@ detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" -diff@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" dom-serializer@0: version "0.1.0" @@ -483,11 +493,11 @@ electron-download@^3.0.1: semver "^5.3.0" sumchecker "^1.2.0" -electron@1.7.7: - version "1.7.7" - resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.7.tgz#cfd89ca9eba79d763ac0b0c6dcc583792097b9b6" +electron@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.6.tgz#8e5c1bd2ebc08fa7a6ee906de3753c1ece9d7300" dependencies: - "@types/node" "^7.0.18" + "@types/node" "^8.0.24" electron-download "^3.0.1" extract-zip "^1.0.3" @@ -699,18 +709,7 @@ glob2base@^0.0.12: dependencies: find-index "^0.1.1" -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.5: +glob@7.1.2, glob@^7.0.5: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -725,13 +724,9 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" har-schema@^1.0.5: version "1.0.5" @@ -765,6 +760,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -865,7 +864,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -979,10 +978,6 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json3@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -1030,52 +1025,9 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" +lodash@^4.16.4: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" lodash@^4.5.1: version "4.17.5" @@ -1092,6 +1044,14 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + meow@^3.1.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -1149,7 +1109,7 @@ mime-types@~2.1.7: dependencies: mime-db "~1.33.0" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -1169,28 +1129,44 @@ mkdirp@0.5.0: dependencies: minimist "0.0.8" -mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1: +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -mocha@^3.2.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" +mocha-junit-reporter@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c" dependencies: - browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.8" - diff "3.2.0" + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" + glob "7.1.2" + growl "1.10.5" he "1.1.1" - json3 "3.3.2" - lodash.create "3.1.1" + minimatch "3.0.4" mkdirp "0.5.1" - supports-color "3.1.2" + supports-color "5.4.0" ms@2.0.0: version "2.0.0" @@ -1726,6 +1702,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -1755,11 +1737,11 @@ sumchecker@^1.2.0: debug "^2.2.0" es6-promise "^4.0.5" -supports-color@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" dependencies: - has-flag "^1.0.0" + has-flag "^3.0.0" supports-color@^0.2.0: version "0.2.0" @@ -1843,9 +1825,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34" +typescript@2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" uid-number@^0.0.6: version "0.0.6" @@ -1895,6 +1877,10 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + xtend@~2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" diff --git a/test/tree/package.json b/test/tree/package.json new file mode 100644 index 00000000000..6826810e73e --- /dev/null +++ b/test/tree/package.json @@ -0,0 +1,13 @@ +{ + "name": "tree", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "koa": "^2.5.1", + "koa-mount": "^3.0.0", + "koa-route": "^3.2.0", + "koa-static": "^5.0.0", + "mz": "^2.7.0" + } +} diff --git a/test/tree/public/index.html b/test/tree/public/index.html new file mode 100644 index 00000000000..3716374c0de --- /dev/null +++ b/test/tree/public/index.html @@ -0,0 +1,66 @@ + + + + + Tree + + + + +

+ + + + + + \ No newline at end of file diff --git a/test/tree/server.js b/test/tree/server.js new file mode 100644 index 00000000000..11ac8e1ac9b --- /dev/null +++ b/test/tree/server.js @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const fs = require('mz/fs'); +const path = require('path'); +const Koa = require('koa'); +const _ = require('koa-route'); +const serve = require('koa-static'); +const mount = require('koa-mount'); + +const app = new Koa(); +const root = path.dirname(path.dirname(__dirname)); + +async function getTree(fsPath, level) { + const element = path.basename(fsPath); + const stat = await fs.stat(fsPath); + + if (!stat.isDirectory() || element === '.git' || element === '.build' || level >= 4) { + return { element }; + } + + const childNames = await fs.readdir(fsPath); + const children = await Promise.all(childNames.map(async childName => await getTree(path.join(fsPath, childName), level + 1))); + return { element, collapsible: true, collapsed: false, children }; +} + +app.use(serve('public')); +app.use(mount('/static', serve('../../out'))); +app.use(_.get('/api/ls', async ctx => { + const relativePath = ctx.query.path; + const absolutePath = path.join(root, relativePath); + + ctx.body = await getTree(absolutePath, 0); +})) + +app.listen(3000); +console.log('http://localhost:3000'); \ No newline at end of file diff --git a/test/tree/yarn.lock b/test/tree/yarn.lock new file mode 100644 index 00000000000..8ed55756e19 --- /dev/null +++ b/test/tree/yarn.lock @@ -0,0 +1,290 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@^1.2.2: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +any-promise@^1.0.0, any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +content-disposition@~0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +cookies@~0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" + dependencies: + depd "~1.1.1" + keygrip "~1.0.2" + +debug@*, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@^2.6.1: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@^1.1.0, depd@~1.1.1, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +destroy@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +error-inject@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + +escape-html@~1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +http-assert@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" + dependencies: + deep-equal "~1.0.1" + http-errors "~1.6.1" + +http-errors@^1.2.8, http-errors@^1.6.3, http-errors@~1.6.1, http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-generator-function@^1.0.3: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +keygrip@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" + +koa-compose@^3.0.0, koa-compose@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-is-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + +koa-mount@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-3.0.0.tgz#08cab3b83d31442ed8b7e75c54b1abeb922ec197" + dependencies: + debug "^2.6.1" + koa-compose "^3.2.1" + +koa-route@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce" + dependencies: + debug "*" + methods "~1.1.0" + path-to-regexp "^1.2.0" + +koa-send@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb" + dependencies: + debug "^3.1.0" + http-errors "^1.6.3" + mz "^2.7.0" + resolve-path "^1.4.0" + +koa-static@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" + dependencies: + debug "^3.1.0" + koa-send "^5.0.0" + +koa@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.5.1.tgz#79f8b95f8d72d04fe9a58a8da5ebd6d341103f9c" + dependencies: + accepts "^1.2.2" + content-disposition "~0.5.0" + content-type "^1.0.0" + cookies "~0.7.0" + debug "*" + delegates "^1.0.0" + depd "^1.1.0" + destroy "^1.0.3" + error-inject "~1.0.0" + escape-html "~1.0.1" + fresh "^0.5.2" + http-assert "^1.1.0" + http-errors "^1.2.8" + is-generator-function "^1.0.3" + koa-compose "^4.0.0" + koa-convert "^1.2.0" + koa-is-json "^1.0.0" + mime-types "^2.0.7" + on-finished "^2.1.0" + only "0.0.2" + parseurl "^1.3.0" + statuses "^1.2.0" + type-is "^1.5.5" + vary "^1.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +methods@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@^2.0.7, mime-types@~2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +on-finished@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +only@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + +parseurl@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +path-is-absolute@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-to-regexp@^1.2.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +resolve-path@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" + dependencies: + http-errors "~1.6.2" + path-is-absolute "1.0.1" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +"statuses@>= 1.4.0 < 2", statuses@^1.2.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + dependencies: + any-promise "^1.0.0" + +type-is@^1.5.5: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +vary@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" diff --git a/tslint.json b/tslint.json index 1fcafd325b6..7f1388dfa76 100644 --- a/tslint.json +++ b/tslint.json @@ -51,7 +51,6 @@ // !!! Do not relax these rules !!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! { - // vs/base/common cannot depend on anything else "target": "**/vs/base/common/**", "restrictions": [ "vs/nls", @@ -59,7 +58,6 @@ ] }, { - // vs/base/test/common contains tests for vs/base/common "target": "**/vs/base/test/common/**", "restrictions": [ "assert", @@ -69,7 +67,6 @@ ] }, { - // vs/base/browser can only depend on vs/base/common "target": "**/vs/base/browser/**", "restrictions": [ "vs/nls", @@ -77,6 +74,14 @@ "**/vs/base/{common,browser}/**" ] }, + { + "target": "**/vs/base/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,browser,node}/**", + "*" // node modules + ] + }, { // vs/base/test/browser contains tests for vs/base/browser "target": "**/vs/base/test/browser/**", @@ -104,6 +109,34 @@ "**/vs/base/parts/*/{common,browser}/**" ] }, + { + "target": "**/vs/base/parts/*/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,browser,node}/**", + "**/vs/base/parts/*/{common,browser,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/base/parts/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,node,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/base/parts/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,browser,node,electron-main}/**", + "**/vs/base/parts/*/{common,browser,node,electron-main}/**", + "*" // node modules + ] + }, { "target": "**/vs/platform/*/common/**", "restrictions": [ @@ -134,6 +167,40 @@ "**/vs/platform/*/{common,browser}/**" ] }, + { + "target": "**/vs/platform/*/node/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,browser,node}/**", + "**/vs/base/parts/*/{common,browser,node}/**", + "**/vs/platform/node/*", + "**/vs/platform/*/{common,browser,node}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,node}/**", + "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", + "**/vs/platform/node/*", + "**/vs/platform/*/{common,browser,node,electron-browser}/**", + "*" // node modules + ] + }, + { + "target": "**/vs/platform/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/vs/base/{common,browser,node}/**", + "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", + "**/vs/platform/node/*", + "**/vs/platform/*/{common,browser,node,electron-main}/**", + "*" // node modules + ] + }, { "target": "**/vs/platform/*/test/browser/**", "restrictions": [ @@ -268,7 +335,6 @@ "target": "**/vs/workbench/common/**", "restrictions": [ "vs/nls", - "vs/css!./**/*", "**/vs/base/common/**", "**/vs/base/parts/*/common/**", "**/vs/platform/*/common/**", @@ -320,7 +386,6 @@ "target": "**/vs/workbench/node/**", "restrictions": [ "vs/nls", - "vs/css!./**/*", "**/vs/base/{common,node}/**", "**/vs/base/parts/*/{common,node}/**", "**/vs/platform/node/**", @@ -351,7 +416,6 @@ "target": "**/vs/workbench/services/**/common/**", "restrictions": [ "vs/nls", - "vs/css!./**/*", "**/vs/base/**/common/**", "**/vs/platform/**/common/**", "**/vs/editor/common/**", @@ -375,7 +439,6 @@ "target": "**/vs/workbench/services/**/node/**", "restrictions": [ "vs/nls", - "vs/css!./**/*", "**/vs/base/**/{common,node}/**", "**/vs/platform/**/{common,node}/**", "**/vs/editor/{common,node}/**", @@ -398,14 +461,13 @@ ] }, { - "target": "**/vs/code/electron-browser/**", + "target": "**/vs/code/node/**", "restrictions": [ "vs/nls", - "vs/css!./**/*", - "vs/nls", - "**/vs/base/**", - "**/vs/platform/**", - "**/vs/code/**", + "**/vs/base/**/{common,browser,node}/**", + "**/vs/base/parts/**/{common,browser,node}/**", + "**/vs/platform/**/{common,browser,node}/**", + "**/vs/code/**/{common,browser,node}/**", "*" // node modules ] }, @@ -414,20 +476,22 @@ "restrictions": [ "vs/nls", "vs/css!./**/*", - "vs/nls", - "**/vs/base/**", - "**/vs/platform/**", - "**/vs/code/**", + "**/vs/base/**/{common,browser,node,electron-browser}/**", + "**/vs/base/parts/**/{common,browser,node,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-browser}/**", + "**/vs/code/**/{common,browser,node,electron-browser}/**", "*" // node modules ] }, { - "target": "**/vs/code/**", + "target": "**/vs/code/electron-main/**", "restrictions": [ "vs/nls", - "**/vs/base/**", - "**/vs/platform/**", - "**/vs/code/**", + "**/vs/base/**/{common,browser,node}/**", + "**/vs/base/parts/**/{common,browser,node,electron-main}/**", + "**/vs/platform/**/{common,browser,node,electron-main}/**", + "**/vs/code/**/{common,browser,node,electron-main}/**", + "**/vs/code/code.main", "*" // node modules ] }, diff --git a/yarn.lock b/yarn.lock index cdd6e17f7d7..1045e7659c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,6 +43,161 @@ version "1.16.34" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.34.tgz#a9761fff33d0f7b3fe61875b577778a2576a9a03" +"@types/tapable@*": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" + +"@types/uglify-js@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.3.tgz#801a5ca1dc642861f47c46d14b700ed2d610840b" + dependencies: + source-map "^0.6.1" + +"@types/webpack@^4.4.10": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.10.tgz#2ecf12589142bc531549140612815b7d8b076358" + dependencies: + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + source-map "^0.6.0" + +"@webassemblyjs/ast@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25" + dependencies: + "@webassemblyjs/helper-module-context" "1.5.13" + "@webassemblyjs/helper-wasm-bytecode" "1.5.13" + "@webassemblyjs/wast-parser" "1.5.13" + debug "^3.1.0" + mamacro "^0.0.3" + +"@webassemblyjs/floating-point-hex-parser@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.13.tgz#29ce0baa97411f70e8cce68ce9c0f9d819a4e298" + +"@webassemblyjs/helper-api-error@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.13.tgz#e49b051d67ee19a56e29b9aa8bd949b5b4442a59" + +"@webassemblyjs/helper-buffer@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.13.tgz#873bb0a1b46449231137c1262ddfd05695195a1e" + dependencies: + debug "^3.1.0" + +"@webassemblyjs/helper-code-frame@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.13.tgz#1bd2181b6a0be14e004f0fe9f5a660d265362b58" + dependencies: + "@webassemblyjs/wast-printer" "1.5.13" + +"@webassemblyjs/helper-fsm@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.13.tgz#cdf3d9d33005d543a5c5e5adaabf679ffa8db924" + +"@webassemblyjs/helper-module-context@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.13.tgz#dc29ddfb51ed657655286f94a5d72d8a489147c5" + dependencies: + debug "^3.1.0" + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.13.tgz#03245817f0a762382e61733146f5773def15a747" + +"@webassemblyjs/helper-wasm-section@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.13.tgz#efc76f44a10d3073b584b43c38a179df173d5c7d" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-buffer" "1.5.13" + "@webassemblyjs/helper-wasm-bytecode" "1.5.13" + "@webassemblyjs/wasm-gen" "1.5.13" + debug "^3.1.0" + +"@webassemblyjs/ieee754@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.5.13.tgz#573e97c8c12e4eebb316ca5fde0203ddd90b0364" + dependencies: + ieee754 "^1.1.11" + +"@webassemblyjs/leb128@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.5.13.tgz#ab52ebab9cec283c1c1897ac1da833a04a3f4cee" + dependencies: + long "4.0.0" + +"@webassemblyjs/utf8@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.5.13.tgz#6b53d2cd861cf94fa99c1f12779dde692fbc2469" + +"@webassemblyjs/wasm-edit@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.13.tgz#c9cef5664c245cf11b3b3a73110c9155831724a8" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-buffer" "1.5.13" + "@webassemblyjs/helper-wasm-bytecode" "1.5.13" + "@webassemblyjs/helper-wasm-section" "1.5.13" + "@webassemblyjs/wasm-gen" "1.5.13" + "@webassemblyjs/wasm-opt" "1.5.13" + "@webassemblyjs/wasm-parser" "1.5.13" + "@webassemblyjs/wast-printer" "1.5.13" + debug "^3.1.0" + +"@webassemblyjs/wasm-gen@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.13.tgz#8e6ea113c4b432fa66540189e79b16d7a140700e" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-wasm-bytecode" "1.5.13" + "@webassemblyjs/ieee754" "1.5.13" + "@webassemblyjs/leb128" "1.5.13" + "@webassemblyjs/utf8" "1.5.13" + +"@webassemblyjs/wasm-opt@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.13.tgz#147aad7717a7ee4211c36b21a5f4c30dddf33138" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-buffer" "1.5.13" + "@webassemblyjs/wasm-gen" "1.5.13" + "@webassemblyjs/wasm-parser" "1.5.13" + debug "^3.1.0" + +"@webassemblyjs/wasm-parser@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.13.tgz#6f46516c5bb23904fbdf58009233c2dd8a54c72f" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-api-error" "1.5.13" + "@webassemblyjs/helper-wasm-bytecode" "1.5.13" + "@webassemblyjs/ieee754" "1.5.13" + "@webassemblyjs/leb128" "1.5.13" + "@webassemblyjs/utf8" "1.5.13" + +"@webassemblyjs/wast-parser@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.5.13.tgz#5727a705d397ae6a3ae99d7f5460acf2ec646eea" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/floating-point-hex-parser" "1.5.13" + "@webassemblyjs/helper-api-error" "1.5.13" + "@webassemblyjs/helper-code-frame" "1.5.13" + "@webassemblyjs/helper-fsm" "1.5.13" + long "^3.2.0" + mamacro "^0.0.3" + +"@webassemblyjs/wast-printer@1.5.13": + version "1.5.13" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.5.13.tgz#bb34d528c14b4f579e7ec11e793ec50ad7cd7c95" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/wast-parser" "1.5.13" + long "^3.2.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -58,6 +213,12 @@ accepts@~1.3.4: mime-types "~2.1.16" negotiator "0.6.1" +acorn-dynamic-import@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" + dependencies: + acorn "^5.0.0" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -72,6 +233,10 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" +acorn@^5.0.0, acorn@^5.6.2: + version "5.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" + acorn@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" @@ -86,6 +251,10 @@ ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" +ajv-keywords@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" + ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -102,6 +271,15 @@ ajv@^5.1.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" +ajv@^6.1.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360" + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.1" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -122,6 +300,12 @@ amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + dependencies: + ansi-wrap "^0.1.0" + ansi-cyan@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" @@ -132,6 +316,10 @@ ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + ansi-gray@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" @@ -170,22 +358,28 @@ ansi-styles@^3.1.0: dependencies: color-convert "^1.9.0" -ansi-wrap@0.1.0: +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" + micromatch "^3.1.4" + normalize-path "^2.1.1" applicationinsights@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-0.18.0.tgz#162ebb48a383408bc4de44db32b417307f45bbc1" -aproba@^1.0.3: +aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -219,14 +413,26 @@ arr-diff@^2.0.0: dependencies: arr-flatten "^1.0.1" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + arr-flatten@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + arr-union@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + array-differ@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" @@ -273,7 +479,11 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" -arrify@^1.0.0: +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -294,9 +504,13 @@ asar@^0.14.0: mksnapshot "^0.3.0" tmp "0.0.28" -asn1@0.1.11: - version "0.1.11" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" asn1@~0.2.3: version "0.2.3" @@ -306,14 +520,20 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" -assert-plus@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" - assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -336,14 +556,14 @@ async@~0.2.8: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" -async@~0.9.0: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +atob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" + atob@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" @@ -367,27 +587,10 @@ aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws-sign@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/aws-sign/-/aws-sign-0.3.0.tgz#3d81ca69b474b1e16518728b51c24ff0bbedc6e9" - aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -azure-storage@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-0.3.3.tgz#5e1920ba75c678cb3f5e52a89136ef36210b58a1" - dependencies: - extend "~1.2.1" - mime "~1.2.4" - node-uuid "~1.4.0" - request "~2.27.0" - underscore "~1.4.4" - validator "~3.1.0" - xml2js "0.2.7" - xmlbuilder "0.4.3" - azure-storage@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-1.4.0.tgz#fb52fa68b3efa6980c33fd7c5cd489b7adc46ed1" @@ -423,6 +626,22 @@ base64-js@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" +base64-js@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -437,6 +656,10 @@ big-integer@^1.6.25: version "1.6.25" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.25.tgz#1de45a9f57542ac20121c682f8d642220a34e823" +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" @@ -473,6 +696,14 @@ bl@~1.1.2: dependencies: readable-stream "~2.0.5" +bluebird@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + body-parser@1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" @@ -492,12 +723,6 @@ boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" -boom@0.4.x: - version "0.4.2" - resolved "https://registry.yarnpkg.com/boom/-/boom-0.4.2.tgz#7a636e9ded4efcefb19cef4947a3c67dfaee911b" - dependencies: - hoek "0.9.x" - boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" @@ -531,14 +756,86 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +braces@^2.3.0, braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + "browser-request@>= 0.3.1 < 0.4.0": version "0.3.3" resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17" +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + browserify-mime@~1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: version "1.7.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" @@ -565,6 +862,22 @@ buffer-fill@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071" +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffers@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" @@ -579,10 +892,46 @@ builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -666,6 +1015,14 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" @@ -674,6 +1031,14 @@ chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chardet@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.5.0.tgz#fe3ac73c00c3d865ffcc02a0682e2c20b6a06029" + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + cheerio@^1.0.0-rc.1: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" @@ -685,6 +1050,25 @@ cheerio@^1.0.0-rc.1: lodash "^4.15.0" parse5 "^3.0.1" +chokidar@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + lodash.debounce "^4.0.8" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.5" + optionalDependencies: + fsevents "^1.2.2" + chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" @@ -696,6 +1080,12 @@ chrome-remote-interface@^0.25.3: commander "2.11.x" ws "3.3.x" +chrome-trace-event@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" + dependencies: + tslib "^1.9.0" + chromium-pickle-js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" @@ -704,6 +1094,13 @@ ci-info@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" @@ -714,6 +1111,15 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + clean-css@3.4.6: version "3.4.6" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.6.tgz#fcb4f17057ddb7f8721616f70b07b294d95ffc45" @@ -727,6 +1133,12 @@ cli-cursor@^1.0.1: dependencies: restore-cursor "^1.0.1" +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -797,6 +1209,13 @@ coffee-script@^1.10.0: version "1.12.7" resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" @@ -847,12 +1266,6 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -combined-stream@~0.0.4: - version "0.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-0.0.7.tgz#0137e657baa5a7541c57ac37ac5fc07d73b4dc1f" - dependencies: - delayed-stream "0.0.5" - commander@*, commander@^2.11.0: version "2.15.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" @@ -875,7 +1288,7 @@ commander@2.8.x: dependencies: graceful-readlink ">= 1.0.0" -commander@^2.12.1: +commander@^2.12.1, commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -883,6 +1296,14 @@ commandpost@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/commandpost/-/commandpost-1.2.1.tgz#2e9c4c7508b9dc704afefaa91cab92ee6054cc68" +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -895,6 +1316,15 @@ concat-stream@1.6.0, concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-with-sourcemaps@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz#f55b3be2aeb47601b10a2d5259ccfb70fd2f1dd6" @@ -908,10 +1338,20 @@ config-chain@~1.1.5: ini "^1.3.4" proto-list "~1.2.1" +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" @@ -924,10 +1364,6 @@ convert-source-map@1.X, convert-source-map@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" -cookie-jar@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/cookie-jar/-/cookie-jar-0.3.0.tgz#bc9a27d4e2b97e186cd57c9e2063cb99fa68cccc" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -936,6 +1372,34 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +copy-webpack-plugin@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz#d53444a8fea2912d806e78937390ddd7e632ee5c" + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + p-limit "^1.0.0" + serialize-javascript "^1.4.0" + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -950,6 +1414,34 @@ coveralls@^2.11.11: minimist "1.2.0" request "2.79.0" +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + cross-spawn@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -965,11 +1457,19 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cryptiles@0.2.x: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-0.2.2.tgz#ed91ff1f17ad13d3748288594f8a48a0d26f325c" +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" dependencies: - boom "0.4.x" + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" cryptiles@2.x.x: version "2.0.5" @@ -983,6 +1483,22 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + cson-parser@^1.3.3: version "1.3.5" resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" @@ -1069,10 +1585,6 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": dependencies: cssom "0.3.x" -ctype@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" - cuint@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" @@ -1083,6 +1595,10 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -1095,6 +1611,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + dateformat@^1.0.11, dateformat@^1.0.7-1.2.3: version "1.0.12" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" @@ -1124,7 +1644,7 @@ debug@2.2.0, debug@~2.2.0: dependencies: ms "0.7.1" -debug@2.6.9, debug@2.X, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0: +debug@2.6.9, debug@2.X, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1140,6 +1660,16 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + dependencies: + xregexp "4.0.0" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -1180,6 +1710,25 @@ defaults@^1.0.0: dependencies: clone "^1.0.2" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -1196,10 +1745,6 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" -delayed-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f" - delayed-stream@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.6.tgz#a2646cb7ec3d5d7774614670a7a65de0c173edbc" @@ -1224,6 +1769,13 @@ deprecated@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -1242,7 +1794,7 @@ detect-indent@^2.0.0: minimist "^1.1.0" repeating "^1.1.0" -detect-libc@^1.0.3: +detect-libc@^1.0.2, detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -1258,6 +1810,21 @@ diff@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -1283,6 +1850,10 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "~1.1.1" entities "~1.1.1" +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + domelementtype@1, domelementtype@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" @@ -1330,6 +1901,15 @@ duplexify@^3.2.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + eachr@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/eachr/-/eachr-3.2.0.tgz#2c35e43ea086516f7997cf80b7aa64d55a4a4484" @@ -1387,6 +1967,22 @@ electron-to-chromium@^1.2.7: version "1.3.27" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" @@ -1403,6 +1999,14 @@ end-of-stream@~0.1.5: dependencies: once "~1.3.0" +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" @@ -1411,6 +2015,12 @@ env-paths@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + dependencies: + prr "~1.0.1" + error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -1522,6 +2132,13 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint@^3.0.0, eslint@^3.4.0: version "3.19.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" @@ -1633,6 +2250,17 @@ event-stream@^3.1.7, event-stream@^3.3.1, event-stream@^3.3.4, event-stream@~3.3 stream-combiner "~0.0.4" through "~2.3.1" +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -1655,6 +2283,18 @@ expand-brackets@^0.1.4: dependencies: is-posix-bracket "^0.1.0" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" @@ -1724,6 +2364,13 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1732,12 +2379,33 @@ extend@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c" +external-editor@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.0.tgz#dc35c48c6f98a30ca27a20e9687d7f3c77704bb6" + dependencies: + chardet "^0.5.0" + iconv-lite "^0.4.22" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" dependencies: is-extglob "^1.0.0" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extract-opts@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/extract-opts/-/extract-opts-3.3.1.tgz#5abbedc98c0d5202e3278727f9192d7e086c6be1" @@ -1778,6 +2446,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -1790,7 +2462,7 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" -fast-plist@0.1.2, fast-plist@^0.1.2: +fast-plist@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" @@ -1807,6 +2479,12 @@ figures@^1.3.5: escape-string-regexp "^1.0.5" object-assign "^4.1.0" +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -1835,6 +2513,15 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -1847,6 +2534,14 @@ finalhandler@1.1.0: statuses "~1.3.1" unpipe "~1.0.0" +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + find-index@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" @@ -1868,6 +2563,12 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + dependencies: + locate-path "^3.0.0" + findup-sync@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" @@ -1908,11 +2609,18 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flush-write-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + for-in@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.5.tgz#007374e2b6d5c67420a1479bdb75a04872b738c4" -for-in@^1.0.1: +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1928,22 +2636,10 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -forever-agent@~0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.5.2.tgz#6d0e09c4921f94a27f63d3b49c5feff1ea4c5130" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@~0.1.0: - version "0.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.1.4.tgz#91abd788aba9702b1aabfa8bc01031a2ac9e3b12" - dependencies: - async "~0.9.0" - combined-stream "~0.0.4" - mime "~1.2.11" - form-data@~1.0.0-rc4: version "1.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" @@ -1978,10 +2674,23 @@ forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -2011,10 +2720,32 @@ fs-extra@^2.0.0: graceful-fs "^4.1.2" jsonfile "^2.1.0" +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" +fsevents@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + function-bind@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2068,6 +2799,10 @@ get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + getmac@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/getmac/-/getmac-1.4.1.tgz#cfefcb3ee7d7a73cba5292129cb100c19afbe17a" @@ -2107,7 +2842,7 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob-parent@^3.0.0: +glob-parent@^3.0.0, glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" dependencies: @@ -2205,6 +2940,10 @@ glob@~3.1.21: inherits "1" minimatch "~0.2.11" +global-modules-path@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.0.tgz#b0e2bac6beac39745f7db5c59d26a36a0b94f7dc" + global-modules@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" @@ -2236,6 +2975,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + globule@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" @@ -2259,7 +3009,7 @@ gm@^1.14.2: cross-spawn "^4.0.0" debug "~2.2.0" -graceful-fs@4.1.11, graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@4.1.11, graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2675,6 +3425,10 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-gulplog@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" @@ -2685,20 +3439,52 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + has@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" dependencies: function-bind "^1.0.2" -hawk@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-1.0.0.tgz#b90bb169807285411da7ffcb8dd2598502d3b52d" +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" dependencies: - boom "0.4.x" - cryptiles "0.2.x" - hoek "0.9.x" - sntp "0.2.x" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" hawk@~3.1.3: version "3.1.3" @@ -2718,9 +3504,13 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -hoek@0.9.x: - version "0.9.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-0.9.1.tgz#3d322462badf07716ea7eb85baf88079cddce505" +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" hoek@2.x.x: version "2.16.3" @@ -2771,14 +3561,6 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-signature@~0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.10.1.tgz#4fbdac132559aa8323121e540779c0a012b27e66" - dependencies: - asn1 "0.1.11" - assert-plus "^0.1.5" - ctype "0.5.3" - http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -2795,6 +3577,10 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + https-proxy-agent@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" @@ -2815,16 +3601,41 @@ iconv-lite@0.4.19, iconv-lite@^0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@0.4.23: +iconv-lite@0.4.23, iconv-lite@^0.4.22, iconv-lite@^0.4.4: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.11, ieee754@^1.1.4: + version "1.1.12" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + ignore@^3.2.0: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + +import-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" + dependencies: + pkg-dir "^2.0.0" + resolve-cwd "^2.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2839,6 +3650,10 @@ indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2884,6 +3699,24 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" +inquirer@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.1.0.tgz#8f65c7b31c498285f4ddf3b742ad8c487892040b" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + int64-buffer@^0.1.9: version "0.1.9" resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.9.tgz#9e039da043b24f78b196b283e04653ef5e990f61" @@ -2892,6 +3725,10 @@ interpret@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" +interpret@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -2915,6 +3752,18 @@ is-absolute@^0.2.3: is-relative "^0.2.1" is-windows "^0.2.0" +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -2929,6 +3778,10 @@ is-buffer@^1.0.2: version "1.1.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" +is-buffer@^1.1.5, is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + is-builtin-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" @@ -2941,6 +3794,34 @@ is-ci@^1.0.9: dependencies: ci-info "^1.0.0" +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + is-dotfile@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" @@ -2955,11 +3836,17 @@ is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -2991,6 +3878,12 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" @@ -3006,6 +3899,12 @@ is-number@^2.0.2, is-number@^2.1.0: dependencies: kind-of "^3.0.2" +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -3022,11 +3921,11 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0: +is-plain-obj@^1.0.0, is-plain-obj@^1.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" -is-plain-object@^2.0.3: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" dependencies: @@ -3040,6 +3939,10 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -3088,6 +3991,10 @@ is-windows@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + is@^3.1.0, is@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" @@ -3235,10 +4142,18 @@ json-edm-parser@0.1.2: dependencies: jsonparse "~1.2.0" +json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3249,10 +4164,14 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: +json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -3297,6 +4216,26 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" +kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -3375,6 +4314,18 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -3382,6 +4333,13 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" @@ -3457,6 +4415,14 @@ lodash._shimkeys@~2.4.1: dependencies: lodash._objecttypes "~2.4.1" +lodash.clone@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + lodash.defaults@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-2.4.1.tgz#a7e8885f05e68851144b6e12a8f3678026bc4c54" @@ -3540,6 +4506,10 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.some@^4.2.2: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + lodash.template@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-2.4.1.tgz#9e611007edf629129a974ab3c48b817b3e1cf20d" @@ -3598,6 +4568,10 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lodash@^4.17.10: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + lodash@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" @@ -3614,6 +4588,14 @@ lolex@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" +long@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + +long@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -3647,6 +4629,12 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + dependencies: + pify "^3.0.0" + make-error-cause@^1.1.1: version "1.2.2" resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" @@ -3657,7 +4645,11 @@ make-error@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96" -map-cache@^0.2.0: +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + +map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -3673,6 +4665,12 @@ map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + markdown-it@^8.3.1: version "8.4.0" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.0.tgz#e2400881bf171f7018ed1bd9da441dac8af6306d" @@ -3687,6 +4685,21 @@ math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -3701,6 +4714,13 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + meow@^3.1.0, meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -3720,6 +4740,12 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merge-options@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-1.0.1.tgz#2a64b24457becd4e4dc608283247e94ce589aa32" + dependencies: + is-plain-obj "^1.1" + merge-stream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" @@ -3748,6 +4774,31 @@ micromatch@^2.1.5, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" +micromatch@^3.1.4, micromatch@^3.1.8: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" @@ -3758,14 +4809,14 @@ mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, dependencies: mime-db "~1.30.0" -mime@1.2.11, mime@~1.2.11, mime@~1.2.4, mime@~1.2.9: - version "1.2.11" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" - mime@1.4.1, mime@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -3774,6 +4825,14 @@ mimic-response@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + minimatch@0.3: version "0.3.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" @@ -3816,6 +4875,41 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + mkdirp@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" @@ -3844,6 +4938,16 @@ mksnapshot@^0.3.0: fs-extra "0.26.7" request "^2.79.0" +mocha-junit-reporter@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c" + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + mocha@^2.0.1, mocha@^2.2.5: version "2.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" @@ -3859,6 +4963,17 @@ mocha@^2.0.1, mocha@^2.2.5: supports-color "1.2.0" to-iso-string "0.0.2" +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -3886,10 +5001,14 @@ mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" -mute-stream@~0.0.4: +mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" +nan@2.10.0, nan@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + nan@2.8.0, nan@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -3898,7 +5017,7 @@ nan@^2.0.0: version "2.9.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866" -nan@^2.0.9, nan@^2.3.0: +nan@^2.0.9: version "2.4.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" @@ -3906,10 +5025,30 @@ nan@^2.1.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" +nan@^2.10.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" + nan@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + native-is-elevated@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.2.1.tgz#70a2123a8575b9f624a3ef465d98cb74ae017385" @@ -3930,21 +5069,80 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" +needle@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +neo-async@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee" + +nice-try@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" + node-abi@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.1.tgz#7628c4d4ec4e9cd3764ceb3652f36b2e7f8d4923" dependencies: semver "^5.4.1" -node-pty@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.4.tgz#07146b2b40b76e432e57ce6750bda40f0da5c99f" +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" dependencies: - nan "^2.6.2" + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-pty@0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.7.tgz#919a9de7f0462ee72b260a6e4d5459d28fde2d25" + dependencies: + nan "2.10.0" node-uuid@~1.4.0, node-uuid@~1.4.7: version "1.4.8" @@ -3972,6 +5170,13 @@ nopt@3.x, nopt@^3.0.1, nopt@~3.0.1: dependencies: abbrev "1" +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -3991,7 +5196,7 @@ normalize-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" -normalize-path@^2.0.0: +normalize-path@^2.0.0, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: @@ -4014,13 +5219,24 @@ normalize-url@^1.4.0: query-string "^4.1.0" sort-keys "^1.0.0" +npm-bundled@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" dependencies: path-key "^2.0.0" -npmlog@^4.0.1: +npmlog@^4.0.1, npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: @@ -4059,10 +5275,6 @@ number-is-nan@^1.0.0: version "1.4.3" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" -oauth-sign@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.3.0.tgz#cb540f93bb2b22a7d5941691a288d60e8ea9386e" - oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -4079,10 +5291,24 @@ object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + object.defaults@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" @@ -4099,7 +5325,7 @@ object.omit@^2.0.0: for-own "^0.1.3" is-extendable "^0.1.1" -object.pick@^1.2.0: +object.pick@^1.2.0, object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" dependencies: @@ -4127,9 +5353,15 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" -oniguruma@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/oniguruma/-/oniguruma-6.1.1.tgz#1c7d96e53d116eb881dbe78b8355b4adc8225898" +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +oniguruma@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/oniguruma/-/oniguruma-7.0.0.tgz#cf258a8b1a2ec1d0d68964d6336df264008ebf4c" dependencies: nan "^2.0.9" @@ -4187,6 +5419,10 @@ ordered-read-streams@^0.3.0: is-stream "^1.0.1" readable-stream "^2.0.1" +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -4199,7 +5435,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4210,6 +5446,13 @@ osenv@^0.1.3: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + p-all@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-all/-/p-all-1.0.0.tgz#93bdf53a55a23821fdfa98b4174a99bf7f31df8d" @@ -4220,18 +5463,36 @@ p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" +p-limit@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + dependencies: + p-try "^1.0.0" + p-limit@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" dependencies: p-try "^1.0.0" +p-limit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" dependencies: p-limit "^1.1.0" +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + dependencies: + p-limit "^2.0.0" + p-map@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -4240,6 +5501,32 @@ p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" +p-try@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + parse-filepath@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.1.tgz#159d6155d43904d16c10ef698911da1e91969b73" @@ -4287,6 +5574,14 @@ parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -4309,7 +5604,7 @@ path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -4339,12 +5634,28 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" dependencies: through "~2.3" +pbkdf2@^3.0.3: + version "3.0.16" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -4361,6 +5672,10 @@ pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -4371,6 +5686,12 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + plist@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593" @@ -4390,6 +5711,15 @@ plugin-error@^0.1.2: arr-union "^2.0.1" extend-shallow "^1.1.2" +plugin-error@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" + dependencies: + ansi-colors "^1.0.1" + arr-diff "^4.0.0" + arr-union "^3.1.0" + extend-shallow "^3.0.2" + plur@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a" @@ -4400,6 +5730,10 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + postcss-calc@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" @@ -4673,6 +6007,10 @@ process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + progress-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" @@ -4684,6 +6022,10 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + promisify-node@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promisify-node/-/promisify-node-0.3.0.tgz#b4b55acf90faa7d2b8b90ca396899086c03060cf" @@ -4701,10 +6043,24 @@ proxy-addr@~2.0.2: forwarded "~0.1.2" ipaddr.js "1.5.2" +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +public-encrypt@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" @@ -4719,17 +6075,33 @@ pump@^1.0.1: end-of-stream "^1.1.0" once "^1.3.1" -pump@^2.0.1: +pump@^2.0.0, pump@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" dependencies: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + q@^1.0.1, q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -4738,10 +6110,6 @@ qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" -qs@~0.6.0: - version "0.6.6" - resolved "https://registry.yarnpkg.com/qs/-/qs-0.6.6.tgz#6e015098ff51968b8a3c819001d5f2c89bc4b107" - qs@~6.2.0: version "6.2.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe" @@ -4761,6 +6129,14 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + queue@3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/queue/-/queue-3.0.6.tgz#66c0ffd0a1d9d28045adebda966a2d3946ab9f13" @@ -4780,6 +6156,19 @@ randomatic@^1.1.3: is-number "^2.0.2" kind-of "^3.0.2" +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -4802,7 +6191,7 @@ rc@^1.1.2: minimist "^1.2.0" strip-json-comments "~2.0.1" -rc@^1.1.6: +rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" dependencies: @@ -4836,6 +6225,18 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" +"readable-stream@1 || 2", readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -4866,18 +6267,6 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" -readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@~2.0.0, readable-stream@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -4940,6 +6329,13 @@ regex-cache@^0.4.2: is-equal-shallow "^0.1.3" is-primitive "^2.0.0" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + remap-istanbul@^0.6.4: version "0.6.4" resolved "https://registry.yarnpkg.com/remap-istanbul/-/remap-istanbul-0.6.4.tgz#ac551eff1aa641504b4f318d0303dda61e3bb695" @@ -4962,6 +6358,10 @@ repeat-string@^1.5.2: version "1.5.4" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.5.4.tgz#64ec0c91e0f4b475f90d5b643651e3e6e5b6c2d5" +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + repeating@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" @@ -5069,23 +6469,6 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@~2.27.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.27.0.tgz#dfb1a224dd3a5a9bade4337012503d710e538668" - dependencies: - aws-sign "~0.3.0" - cookie-jar "~0.3.0" - forever-agent "~0.5.0" - form-data "~0.1.0" - hawk "~1.0.0" - http-signature "~0.10.0" - json-stringify-safe "~5.0.0" - mime "~1.2.9" - node-uuid "~1.4.0" - oauth-sign "~0.3.0" - qs "~0.6.0" - tunnel-agent "~0.3.0" - request@~2.74.0: version "2.74.0" resolved "https://registry.yarnpkg.com/request/-/request-2.74.0.tgz#7693ca768bbb0ea5c8ce08c084a45efa05b892ab" @@ -5127,6 +6510,12 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + dependencies: + resolve-from "^3.0.0" + resolve-dir@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" @@ -5138,7 +6527,11 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" -resolve-url@~0.2.1: +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve-url@^0.2.1, resolve-url@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -5159,6 +6552,17 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -5171,7 +6575,7 @@ rimraf@^2.2.8: dependencies: glob "^7.0.5" -rimraf@^2.4.2: +rimraf@^2.4.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -5181,20 +6585,55 @@ rimraf@~2.2.6: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" dependencies: once "^1.3.0" +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" +rxjs@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.2.tgz#eb75fa3c186ff5289907d06483a77884586e1cf9" + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@^5.1.0, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5211,10 +6650,17 @@ sax@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.2.tgz#735ffaa39a1cff8ffb9598f0223abdb03a9fb2ea" -sax@>=0.6.0, sax@~1.2.1: +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +schema-utils@^0.4.4, schema-utils@^0.4.5: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + semaphore@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.0.5.tgz#b492576e66af193db95d65e25ec53f5f19798d60" @@ -5227,7 +6673,7 @@ semver@^4.1.0, semver@^4.3.4: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" -semver@^5.4.1, semver@^5.5.0: +semver@^5.0.1, semver@^5.4.1, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -5253,6 +6699,10 @@ sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" +serialize-javascript@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" + serve-static@1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" @@ -5270,6 +6720,28 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" @@ -5278,6 +6750,13 @@ setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -5300,7 +6779,7 @@ sigmund@^1.0.1, sigmund@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" -signal-exit@^3.0.0: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -5331,15 +6810,40 @@ sinon@^1.17.2: samsam "1.1.2" util ">=0.10.3 <1" +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" -sntp@0.2.x: - version "0.2.4" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-0.2.4.tgz#fb885f18b0f3aad189f824862536bceeec750900" +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" dependencies: - hoek "0.9.x" + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" sntp@1.x.x: version "1.0.9" @@ -5359,6 +6863,10 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + source-map-resolve@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" @@ -5368,6 +6876,20 @@ source-map-resolve@^0.3.0: source-map-url "~0.3.0" urix "~0.1.0" +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + source-map-url@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" @@ -5378,7 +6900,7 @@ source-map@0.4.x, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@0.X, source-map@>=0.5.6, source-map@^0.6.1, source-map@~0.6.1: +source-map@0.X, source-map@>=0.5.6, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -5402,9 +6924,9 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spdlog@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.6.0.tgz#20632ed4f1558ffa46e8a5827a5e97c61e0fa9ed" +spdlog@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.2.tgz#9298753d7694b9ee9bbfd7e01ea1e4c6ace1e64d" dependencies: bindings "^1.3.0" mkdirp "^0.5.1" @@ -5428,6 +6950,12 @@ speedometer@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -5452,6 +6980,19 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + dependencies: + safe-buffer "^5.1.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + "statuses@>= 1.3.1 < 2": version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" @@ -5460,6 +7001,13 @@ statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -5470,6 +7018,23 @@ stream-consume@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" @@ -5496,13 +7061,19 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -5513,12 +7084,6 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - dependencies: - safe-buffer "~5.1.0" - stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -5613,6 +7178,12 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" +supports-color@^5.3.0, supports-color@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -5636,6 +7207,10 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" +tapable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" + tar-fs@^1.13.0: version "1.16.2" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.2.tgz#17e5239747e399f7e77344f5f53365f04af53577" @@ -5657,6 +7232,18 @@ tar-stream@^1.1.2: to-buffer "^1.1.0" xtend "^4.0.0" +tar@^4: + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + temp@^0.8.1, temp@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" @@ -5725,7 +7312,7 @@ through2@~0.2.3: readable-stream "~1.1.9" xtend "~2.1.1" -through@2, through@^2.3.4, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.8: +through@2, through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5739,6 +7326,12 @@ time-stamp@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" +timers-browserify@^2.0.4: + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" + dependencies: + setimmediate "^1.0.4" + tmp@0.0.28: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" @@ -5751,12 +7344,22 @@ tmp@0.0.29: dependencies: os-tmpdir "~1.0.1" +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + to-absolute-glob@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f" dependencies: extend-shallow "^2.0.1" +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" @@ -5765,6 +7368,28 @@ to-iso-string@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + touch@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/touch/-/touch-0.0.3.tgz#51aef3d449571d4f287a5d87c9c8b49181a0db1d" @@ -5789,6 +7414,16 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" +ts-loader@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-4.4.2.tgz#778d4464b24436873c78f7f9e914d88194c2a248" + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^3.1.4" + semver "^5.0.1" + tslib@^1.7.1: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" @@ -5797,6 +7432,10 @@ tslib@^1.8.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + tslint@^5.9.1: version "5.9.1" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae" @@ -5820,16 +7459,16 @@ tsutils@^2.12.1: dependencies: tslib "^1.7.1" +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" dependencies: safe-buffer "^5.0.1" -tunnel-agent@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.3.0.tgz#ad681b68f5321ad2827c4cfb1b7d5df2cfe942ee" - tunnel-agent@~0.4.1: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" @@ -5879,9 +7518,9 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961" +typescript@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" typescript@^2.6.2: version "2.6.2" @@ -5898,6 +7537,13 @@ uglify-es@^3.0.18: commander "~2.11.0" source-map "~0.6.1" +uglify-es@^3.3.4: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -5918,6 +7564,19 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" +uglifyjs-webpack-plugin@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00" + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + schema-utils "^0.4.5" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + uglify-es "^3.3.4" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" @@ -5934,6 +7593,15 @@ underscore@~1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -5948,6 +7616,18 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" @@ -5963,6 +7643,23 @@ unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + +uri-js@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + dependencies: + punycode "^2.1.0" + urix@^0.1.0, urix@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" @@ -5971,6 +7668,17 @@ url-join@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -5985,12 +7693,18 @@ util-deprecate@1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -"util@>=0.10.3 <1": +util@0.10.3, "util@>=0.10.3 <1": version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" dependencies: inherits "2.0.1" +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + dependencies: + inherits "2.0.3" + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -5999,6 +7713,10 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +v8-compile-cache@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" + v8-inspect-profiler@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/v8-inspect-profiler/-/v8-inspect-profiler-0.0.8.tgz#4d6bedb7c3d1bfc69e5bfdc2ded3d6784a5a76a6" @@ -6022,10 +7740,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -validator@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-3.1.0.tgz#2ea1ff7e92254d69367f385f015299e5ead8755b" - validator@~3.22.2: version "3.22.2" resolved "https://registry.yarnpkg.com/validator/-/validator-3.22.2.tgz#6f297ae67f7f82acc76d0afdb49f18d9a09c18c0" @@ -6139,15 +7853,21 @@ vinyl@~2.0.1: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vsce@1.33.2: - version "1.33.2" - resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.33.2.tgz#3645f69aaf984e22f74ea49d35f38dd18d66ff5f" +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +vsce@1.48.0: + version "1.48.0" + resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.48.0.tgz#31c1a4c6909c3b8bdc48b3d32cc8c8e94c7113a2" dependencies: cheerio "^1.0.0-rc.1" commander "^2.8.1" denodeify "^1.2.1" glob "^7.0.6" - lodash "^4.15.0" + lodash "^4.17.10" markdown-it "^8.3.1" mime "^1.3.4" minimatch "^3.0.3" @@ -6157,15 +7877,21 @@ vsce@1.33.2: semver "^5.1.0" tmp "0.0.29" url-join "^1.1.0" - vso-node-api "^6.1.2-preview" + vso-node-api "6.1.2-preview" yauzl "^2.3.1" yazl "^2.2.2" -vscode-chokidar@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/vscode-chokidar/-/vscode-chokidar-1.6.2.tgz#4db06e2d963befe42dd44434212f5b8606b53831" +vscode-anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/vscode-anymatch/-/vscode-anymatch-1.3.2.tgz#fff357422ea59cc4d12a074f347957f08d251003" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +vscode-chokidar@1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/vscode-chokidar/-/vscode-chokidar-1.6.4.tgz#03e5b5f755a1e73b4f15310e66f59b11673fbdd2" dependencies: - anymatch "^1.3.0" async-each "^1.0.0" glob-parent "^2.0.0" inherits "^2.0.1" @@ -6173,22 +7899,23 @@ vscode-chokidar@1.6.2: is-glob "^2.0.0" path-is-absolute "^1.0.0" readdirp "^2.0.0" + vscode-anymatch "^1.3.0" optionalDependencies: - vscode-fsevents "0.3.8" + vscode-fsevents "0.3.9" -vscode-debugprotocol@1.28.0: - version "1.28.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz#b9fb97c3fb2dadbec78e5c1619ff12bf741ce406" +vscode-debugprotocol@1.32.0-pre.0: + version "1.32.0-pre.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.32.0-pre.0.tgz#14b3c07b8d7016f91b6984ec18292c18a42d2705" -vscode-fsevents@0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/vscode-fsevents/-/vscode-fsevents-0.3.8.tgz#389647fa2f6daffedf1b40132a5bb96ac6501521" +vscode-fsevents@0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/vscode-fsevents/-/vscode-fsevents-0.3.9.tgz#edbb66ea2c4eeb102b9194bb602e73bd9512c64c" dependencies: - nan "^2.3.0" + nan "^2.10.0" -vscode-nls-dev@3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.0.7.tgz#8cfbb371cb3c8f47f247073d9f84a6af357bbfe0" +vscode-nls-dev@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.2.2.tgz#5855c9b3e566dd00fd6108f9c2e1bd02c925c153" dependencies: clone "^2.1.1" event-stream "^3.3.4" @@ -6212,22 +7939,21 @@ vscode-nsfw@1.0.17: nan "^2.0.0" promisify-node "^0.3.0" -vscode-ripgrep@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.0.1.tgz#eff2f2b2a49921ac0acd3ff8dfecaaeebf0184cf" +vscode-ripgrep@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.1.0.tgz#93c1e39d88342ee1b15530a12898ce930d511948" -vscode-textmate@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-3.3.3.tgz#8d737f2965046503a4088d47c2793aebe246d355" +vscode-textmate@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-4.0.1.tgz#6c36f28e9059ce12bc34907f7a33ea43166b26a8" dependencies: - fast-plist "^0.1.2" - oniguruma "^6.0.1" + oniguruma "^7.0.0" -vscode-xterm@3.5.0-beta8: - version "3.5.0-beta8" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.5.0-beta8.tgz#beaf158e00ce8341caa18703b3d1ed18be78b676" +vscode-xterm@3.8.0-beta2: + version "3.8.0-beta2" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.8.0-beta2.tgz#e6f5149199e757c2fb908aaf64f4c0749f216186" -vso-node-api@^6.1.2-preview: +vso-node-api@6.1.2-preview: version "6.1.2-preview" resolved "https://registry.yarnpkg.com/vso-node-api/-/vso-node-api-6.1.2-preview.tgz#aab3546df2451ecd894e071bb99b5df19c5fa78f" dependencies: @@ -6236,6 +7962,81 @@ vso-node-api@^6.1.2-preview: typed-rest-client "^0.9.0" underscore "^1.8.3" +watchpack@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +webpack-cli@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.1.0.tgz#d71a83687dcfeb758fdceeb0fe042f96bcf62994" + dependencies: + chalk "^2.4.1" + cross-spawn "^6.0.5" + enhanced-resolve "^4.0.0" + global-modules-path "^2.1.0" + import-local "^1.0.0" + inquirer "^6.0.0" + interpret "^1.1.0" + loader-utils "^1.1.0" + supports-color "^5.4.0" + v8-compile-cache "^2.0.0" + yargs "^12.0.1" + +webpack-sources@^1.0.1, webpack-sources@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-stream@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-5.1.1.tgz#15b1d91da6887a37f6832128383ae0282bd7d0e7" + dependencies: + fancy-log "^1.3.2" + lodash.clone "^4.3.2" + lodash.some "^4.2.2" + memory-fs "^0.4.1" + plugin-error "^1.0.1" + supports-color "^5.3.0" + through "^2.3.8" + vinyl "^2.1.0" + webpack "^4.7.0" + +webpack@^4.16.5, webpack@^4.7.0: + version "4.16.5" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.16.5.tgz#29fb39462823d7eb8aefcab8b45f7f241db0d092" + dependencies: + "@webassemblyjs/ast" "1.5.13" + "@webassemblyjs/helper-module-context" "1.5.13" + "@webassemblyjs/wasm-edit" "1.5.13" + "@webassemblyjs/wasm-opt" "1.5.13" + "@webassemblyjs/wasm-parser" "1.5.13" + acorn "^5.6.2" + acorn-dynamic-import "^3.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^0.4.4" + tapable "^1.0.0" + uglifyjs-webpack-plugin "^1.2.4" + watchpack "^1.5.0" + webpack-sources "^1.0.1" + whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" @@ -6281,6 +8082,10 @@ windows-process-tree@0.2.2: dependencies: nan "^2.6.2" +winreg@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" + wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -6293,6 +8098,12 @@ wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +worker-farm@^1.5.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" + dependencies: + errno "~0.1.7" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" @@ -6335,6 +8146,10 @@ xml2js@^0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + xmlbuilder@0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58" @@ -6357,6 +8172,10 @@ xmldom@0.1.x: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -6375,10 +8194,24 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + dependencies: + camelcase "^4.1.0" + yargs-parser@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" @@ -6402,6 +8235,23 @@ yargs@^10.1.1: y18n "^3.2.1" yargs-parser "^8.1.0" +yargs@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.1.tgz#6432e56123bb4e7c3562115401e98374060261c2" + dependencies: + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" @@ -6424,7 +8274,7 @@ yauzl@^2.2.1, yauzl@^2.3.1, yauzl@^2.9.1: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" -yazl@^2.2.1, yazl@^2.2.2: +yazl@^2.2.1, yazl@^2.2.2, yazl@^2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" dependencies: